LINUX.ORG.RU

Задачка на самый быстрый подсчет встречаемости слов

 


2

2

Привет,

По мотивам темы: Различия между macOS и GNU/Linux

Есть файлик. Вот он: https://disk.yandex.ru/d/XaavsEkOvCT4HQ

Нужно пройтись по файлику и посчитать встречаемость каждого слова в тексте. Словом считается любая последовательность букв от a до z. Регистр нужно привести к одному. Любой другой символ прерывает слово.

Результат записать в другой файл в формате: <количество> <слово>. Например, текст: «cat, cat, cat». Ответ будет такой: «3 cat». Также, слова при выводе нужно отсортировать по их встречаемости.

Например первые несколько строк вывода из приведенного выше файла будут такими:

3343241 the
1852717 and
1715705 of
1560152 to
1324244 a

Дополнительное условие, нужно, чтобы ваша программа отрабатывала быстрее, чем за 7 секунд на Core i5-4690 @ 3.7 GHz.

Понятно, что железяки у всех разные, поэтому если участвуете, пишите на каком оборудовании вы запускали свою программку.

Вроде как BceM_IIpuBeT хотел поучаствовать. Может еще кто-то присоединится.

Я свою штуку написал. Отрабатывает примерно за 5 секунд на Мак-мини 2012-го года, core i7 @ 2,3 GHz.

★★★★★

Последнее исправление: hibou (всего исправлений: 1)

не вполне в тему, но всё-же спрошу; а вот нахуа?

считать число слов из алфавита a-z ?? ЗАЧЕМ ?

может у кого на практике подобное встретилось, поделитесь историей успеха… или в портфолио годная запись «сделал софт как wc но ограниченно другой» и 100500 лайков.

мне вот даже лень скачивать файл - он большой и всё в итоге зависит от IO.

ЗЫ,в старые времена то да, при лимите в 640K, решение стоило того чтобы подумать.

MKuznetsov ★★★★★
()
Ответ на: комментарий от MKuznetsov

Ну как минимум - развивать творческие способности, чтобы потом не показывать хозяину портфолио в расчёте на тёплое место.

Ну и с практической стороны - вполне возможно заюзать - получать какую-то выжимку из книг/фильмов и тп и скармливать в нейросеть для классификации по жанрам/степени научности/красноречивости/чего угодно ещё, или сформировать словарь своим ученикам ин. языка для учебного фильма.

ЗЫ,в старые времена то да, при лимите в 640K, решение стоило того чтобы подумать.

Производительность одного ядра давно особо не растёт вместе с частотами ЦПУ, разница между скоростью одного ядра на топовых числодробилках и 13 летнем проце отличается на около 2 раз. И тем не менее - знаешь почему перестал юзать oneshot и ranger? Г-о тормозное. А Makehuman оставило лишь отрицательные впечатления по всё тем же причинам.

kvpfs ★★
()
Ответ на: комментарий от MKuznetsov

считать число слов из алфавита a-z

/зевая/ на паскале чтоли напейсать...? размять ягодицы...
там в задании файл на эфиопском каком то

superuser ★★★★☆
()
Последнее исправление: superuser (всего исправлений: 1)

чтобы ваша программа отрабатывала быстрее, чем за 7 секунд

bash успеет при условии не больше мульена строк. :)

Bootmen ☆☆☆
()

Нет желания ни кого обидеть, но теперь понял почему Linux на месте топчится …

anonymous
()
Ответ на: комментарий от Bootmen
$ wc -l huge.txt
6455997 huge.txt

$ wc -l words.txt
58801281 words.txt

Второй файл — отдельное «слово» в каждой строке. Осталось посчитать и отсортировать ахвхвх. Так как из huge.txt «слова» парсятся достаточно быстро, даже в один поток (не больше секунды), то примем, что у тебя есть 6 сек. на 58е6 строк. Справишься?

anonymous
()

А чего все через хешмапы делают? Неужели нет других вариантов? Тогда, если мы меряем языки и их стандартные библиотеки, то надо утвердить общий алгоритм. Но в этом случае получается, что задача решена и дальнейшие действия сводятся к поиску идеальной хеш-функци.

xxhash уже пробовали?

anonymous
()
lve@gw3: ~/src/wc$ /usr/bin/time -f "%E %U %S %M"  ./a 
3342166 the
1851942 and
1715610 of
1559709 to
1323954 a
 956950 in
 932225 i

0:02.06 1.99 0.06 462832

AMD Ryzen 5 3400G 3700MHz.

Голый C с использованием qsort(), чтение файла целиком через mmap.

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

char *in_file="huge.txt";

static uint8_t aho_lc[256] = {
  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,  15,
 16,  17,  18,  19,  20,  21,  22,  23,  24,  25,  26,  27,  28,  29,  30,  31,
 32,  33,  34,  35,  36,  37,  38,  39,  40,  41,  42,  43,  44,  45,  46,  47,
 48,  49,  50,  51,  52,  53,  54,  55,  56,  57,  58,  59,  60,  61,  62,  63,
 64, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',  91,  92,  93,  94,  95,
 96,  97,  98,  99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127,
};

struct node {
    struct node *next[32];
    int count;
} root = { .next = {NULL,}, .count = 0};

struct word {
    int count;
    char *t;
};
typedef struct word word_t;

word_t *all_words = NULL;
int all_words_count = 0;
word_t *words_last;

char d_buf[1024];

void dump(struct node *n,int pos,char *buf,size_t l) {
    int i;
    if(pos >= l) return;
    for(i=0;i < 32; i++) {
       char c = 'a'+i;
       if(n->next[i]) {
        buf[pos] = c;
        dump(n->next[i],pos+1,buf,l);
       }
    }
    buf[pos] = 0;
    if(n->count) {
        words_last->count = n->count;
        words_last->t = strdup(buf);
        words_last++;
    }
    return;
}
int cmp_cnt(const void *a, const void *b) {
    return ((word_t *)b)->count - ((word_t *)a)->count;
}

int main(int argc,char **argv) {

char *buf,*i,*end,c;
int fd,na,ci;
struct stat st;
struct node *n;

    if(argv[1]) in_file = argv[1];
    if(stat(in_file,&st)) abort();
    fd=open(in_file,O_RDONLY);
    if(fd < 0) abort();

    buf = mmap(NULL,st.st_size,PROT_READ,MAP_SHARED,fd,0);
    if(buf == MAP_FAILED) abort();

    end = buf + st.st_size;
    for(i=buf,n = &root; i < end; i++) {
        c = *i;
        na = 1;
        if((c & 0xC0) != 0x80) {
            c = (char)aho_lc[(uint8_t)c];
            if(c >= 'a' && c <= 'z')
                na = 0;
        } else
            i++;
        if(na) {
            if(n != &root) {
                if(!n->count) all_words_count++;
                n->count++; n = &root;
            }
            continue;
        }
        ci = c - 'a';
        if(ci >= 32) abort();
        if(!n->next[ci]) {
            n->next[ci] = (struct node*)calloc(1,sizeof(struct node));
            if(!n->next[ci]) abort();
        }
        n = n->next[ci];
    }
    munmap(buf,st.st_size);
    close(fd);
    all_words = (struct word *)calloc(all_words_count,sizeof(struct word));
    words_last = all_words;
    dump(&root,0,d_buf,sizeof(d_buf)-1);

    qsort(all_words,all_words_count,sizeof(word_t),cmp_cnt);
    for(ci = 0; ci < all_words_count; ci++) {
        printf("%7d %s\n",all_words[ci].count,all_words[ci].t);
        if(ci >= 6) break;
    }
    return 0;
}
vel ★★★★★
()

В догонку:

0:03.15 3.03 0.12 462816 Xeon E5620 @ 2.40GHz (1.6GHz)

0:01.98 1.91 0.06 462848 Xeon E3-1230 V2 @ 3.30GHz (3.3GHz)

0:01.53 1.49 0.03 462788 Xeon E-2136 CPU @ 3.30GHz (4.2GHz)

vel ★★★★★
()
Ответ на: комментарий от vel

У тебя классное решение, очень быстрое, но где-то, видимо, есть ошибка, раз неправильно считает. Я попробовал с xxhash и кастомной функцией сравнения, получилось 3 секунды. Твой вариант считает за 1.88 сек.

В нижний регистр я переводил так: c = text[i] | 0x20;.

anonymous
()
Ответ на: комментарий от vel

Ага, разобрался. Всё верно у тебя, если исходить из того, что файл в UTF-8 (на самом деле нет). Извини за наезд :) Классный вариант.

anonymous
()
Ответ на: комментарий от vel
@@ -80 +80 @@
-        c = *i;
+        c = *i | 0x20;
@@ -82,2 +82,2 @@
-        if((c & 0xC0) != 0x80) {
-            c = (char)aho_lc[(uint8_t)c];
+        /* if((c & 0xC0) != 0x80) {
+            c = (char)aho_lc[(uint8_t)c]; */
@@ -86,2 +86,2 @@
-        } else
-            i++;
+        /* } else
+            i++;*/

Соответственно, LUT не нужна; не стал её удалять, чтобы в diff не попала.

anonymous
()
Ответ на: комментарий от kvpfs

Кстати, мой вариант невалиден - я не учёл, что в файле 8 млн ЮТФ символов, а не голое ASCII. По идее если учесть, то и работать должно быстрее, но время на это нет.

kvpfs ★★
()
Ответ на: комментарий от kvpfs

Задача проста как грабли.

Построить словарь в виде дерева и отсортировать по счетчику.

А какие еще есть варианты? Я не осилил чтение всего треда.

Задачу можно было существенно усложнить, если бы символы «слова» были бы в utf-8, а не только a-z, а так же ввести требование минимизации требуемой памяти. Трюк с mmap() на файл более 2-х гигов в 32-битных системах уже не пройдет.

vel ★★★★★
()
Ответ на: комментарий от vel

А какие еще есть варианты?

Ну мы тут хэш мепы делали. Вообще это более очевидно и, наверное, надёжнее, так как при неблагоприятном раскладе памяти не хватит при таком подходе.

Трюк с mmap()

На самом оделе от mmap ничего не выигрывается в сравнении с чтением всего файла в буфуер и дальнейшей работы с ним, это проверял.

kvpfs ★★
()
Ответ на: комментарий от vel

А какие еще есть варианты?

Пытался городить огород с 26-ю списками от первой буквы (i = c - 'a'). Запутался напрочь. Даже не близко выходило к варианту c++ с хэшами (

Вы - молодец.

Toxo2 ★★★★
()
Ответ на: комментарий от MKuznetsov

может у кого на практике подобное встретилось

Единственное практическое применение которое я вижу - это elastic search. Но гоняться за тактами конкретно здесь - смысла мне кажется нет.

bugfixer ★★★★★
()
Ответ на: комментарий от anonymous

Конкретно эта строчка вообще смысла не имеет и при оптимизации компилятор её выкидывает. А вот размер массива указателей в ноде действительно можно сократить — будет меньше памяти кушать.

Но, как правильно заметил @kvpfs, например, в случае с равномерным распределением букв, алгоритм будет потреблять много памяти и будет медленнее варианта с хешмапом.

$ ls -sh random.txt 
50M random.txt

$ /bin/time -f "%E %U %S %M" ./vel random.txt                
  92364 p
  92318 u
  ...
0:03.53 2.91 0.60 2716716
anonymous
()

А что, это можно без деревьев сделать?

Ну, разве что на 16-битных хешах, с массивом эдаких «кустиков».

anonymous
()

Интересен эффективный алгоритм, использующий tree …

anonymous
()

Решение влоб на коленке, не мудурсвая лукаво. На M1:

5.49s user 0.38s system 98% cpu 5.966 total

package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"
)

type word struct {
	s string
	n int
}

type byCount []word

func (w byCount) Len() int               { return len(w) }
func (w byCount) Less(i int, j int) bool { return w[i].n < w[j].n }
func (w byCount) Swap(i int, j int)      { w[i], w[j] = w[j], w[i] }

func main() {
	f, err := os.Open("huge.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()
	s := bufio.NewScanner(f)
	s.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
		start := 0
		for width := 0; start < len(data); start += width {
			var r rune
			r, width = utf8.DecodeRune(data[start:])
			if unicode.IsLetter(r) {
				break
			}
		}
		for width, i := 0, start; i < len(data); i += width {
			var r rune
			r, width = utf8.DecodeRune(data[i:])
			if !unicode.IsLetter(r) {
				return i + width, data[start:i], nil
			}
		}
		if atEOF && len(data) > start {
			return len(data), data[start:], nil
		}
		return start, nil, nil
	})
	m := make(map[string]int)
	for s.Scan() {
		m[strings.ToLower(s.Text())]++
	}
	var w []word
	for k, v := range m {
		w = append(w, word{s: k, n: v})
	}
	sort.Sort(sort.Reverse(byCount(w)))
	o, err := os.Create("out.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer o.Close()
	for _, v := range w {
		fmt.Fprintln(o, v.n, v.s)
	}
}
3343163 the
1852685 and
1715662 of
1559874 to
1320986 a
956891 in
933115 i
781270 he
713514 that
690876 was
beastie ★★★★★
()
Ответ на: комментарий от beastie

А сможете её распаралелить?

У меня на Go примерно такое же с map, только без юникода - просто байты ASCII.

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"sort"
)

type StringIntStruct struct {
	k string
	v int
}

type ArrayOfStringIntStruct []StringIntStruct

func (a ArrayOfStringIntStruct) Len() int           { return len(a) }
func (a ArrayOfStringIntStruct) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ArrayOfStringIntStruct) Less(i, j int) bool { return a[i].v > a[j].v }

func main() {
	content, err := ioutil.ReadFile("huge.txt")
	if err != nil {
		log.Fatal(err)
	}
	map_words := make(map[string]int)
	var word [1024]byte
	w := 0
	for _, c := range content {
		c = c | 32
		if c >= 'a' && c <= 'z' {
			word[w] = c
			w += 1
			continue
		}
		if w > 0 {
			map_words[string(word[0:w])] += 1
			w = 0
		}
	}
	count_words := len(map_words)
	array_words := make(ArrayOfStringIntStruct, count_words)
	w = 0
	for k, v := range map_words {
		array_words[w] = StringIntStruct{k, v}
		w++
	}
	sort.Sort(array_words)
	o, err := os.Create("out.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer o.Close()
	for _, v := range array_words {
		fmt.Fprintln(o, v.v, v.k)
	}
}
Когда пытаюсь заполнять в несколько goroutine получается только хуже, потому что приходится mutex.Lock перед каждым map, и Unlock после. Если не сложно - как правильно загрузить все ядра в Go в варианте с map?

Ну, так хоть в два раза быстрее варианта на php получилось, и то слава богу. Правда в два раза медленнее вариантов на c и c++.

Toxo2 ★★★★
()
Последнее исправление: Toxo2 (всего исправлений: 4)

ваша программа отрабатывала быстрее, чем за 7 секунд на Core i5-4690 @ 3.7 GHz

Этот тест просто на грамотное владение языком и стандартной либой, потому что обычный один поток с++ и обычный std unordered_map + второй map для сортировки вполне в этот промежуток входит. Миниму кода и при этом ничего дополнительного изобретать не нужно. Да и задачи сделать лучшего решения по времени не было (для этого бы автор просто по времени зарезал условие изначально) тогда уже понятно что нужно переходить на асинхронность.

anonymous2 ★★★★★
()
Последнее исправление: anonymous2 (всего исправлений: 3)
Ответ на: комментарий от Toxo2

потому что приходится mutex.Lock перед каждым map, и Unlock после

Сделай отдельный map для каждого треда, потом сложи. Но я сомневаюсь что здесь можно что-то выгадать. Слишком маленькая задача.

PS: другой вариант, парсер параллелить, но не обновлять в нём, а запихивать в канал найденное слово и считать в одной горутине.

no-such-file ★★★★★
()
Последнее исправление: no-such-file (всего исправлений: 1)
Ответ на: комментарий от no-such-file

Сделай отдельный map для каждого треда, потом сложи.

Да, спасибо, догадался ) Вроде работает:

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"runtime"
	"sort"
	"sync"
)

type StringIntStruct struct {
	k string
	v int
}

type ArrayOfStringIntStruct []StringIntStruct

func (a ArrayOfStringIntStruct) Len() int           { return len(a) }
func (a ArrayOfStringIntStruct) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ArrayOfStringIntStruct) Less(i, j int) bool { return a[i].v > a[j].v }

func fill_data(wg *sync.WaitGroup, data []byte, start int, stop int, m map[string]int) {
	defer wg.Done()
	var word [1024]byte
	w := 0
	for i := start; i <= stop; i++ {
		c := data[i] | 32
		if c >= 'a' && c <= 'z' {
			word[w] = c
			w += 1
			continue
		}
		if w > 0 {
			m[string(word[0:w])]++
			w = 0
		}
	}

}

func main() {
	var wg sync.WaitGroup
	content, err := ioutil.ReadFile("huge.txt")
	if err != nil {
		log.Fatal(err)
	}
	len_content := len(content)
	num_of_cores := runtime.NumCPU()
	arr_map_words := make([]map[string]int, num_of_cores)
	part_content := len_content / num_of_cores
	var part_start, part_stop int
	part_start = 0
	for core := 1; core <= num_of_cores; core++ {
		part_stop = part_content * core
		var bound_sym byte
		for {
			if part_stop >= len_content-1 {
				part_stop = len_content - 1
				break
			}
			bound_sym = content[part_stop] | 32
			if bound_sym < 'a' || bound_sym > 'z' {
				break
			}
			part_stop++
		}
		arr_map_words[core-1] = make(map[string]int)
		wg.Add(1)
		go fill_data(&wg, content, part_start, part_stop, arr_map_words[core-1])
		part_start = part_stop + 1
	}
	wg.Wait()
	map_words := arr_map_words[0]
	for core := 1; core < num_of_cores; core++ {
		for k, v := range arr_map_words[core] {
			map_words[k] += v
		}
	}
	count_words := len(map_words)
	array_words := make(ArrayOfStringIntStruct, count_words)
	w := 0
	for k, v := range map_words {
		array_words[w] = StringIntStruct{k, v}
		w++
	}
	sort.Sort(array_words)

	for i, word := range array_words {
		if i >= 20 {
			break
		}
		fmt.Printf("%v   %v\n", word.v, word.k)
	}
	fmt.Printf("------------\nCount words: %d\n", count_words)
}

Но я сомневаюсь что здесь можно что-то выгадать.

Да нет, хорошо так помогает. На i3-2100 CPU @ 3.10GHz в один поток получается real 0m5,727s, а во все четыре - real 0m3,092s. Чуть не в два раза )

Я там, возможно, где-то ошибся в логике. Но принцип более/менее понял вроде ) Кажется теперь наигрался в эту игру.

--------------
А, ну для сравнения: c от vel в один поток: real 0m2,309s, с++ от kvpfs в четыре потока: real 0m2,665s

Toxo2 ★★★★
()
Последнее исправление: Toxo2 (всего исправлений: 1)
Ответ на: комментарий от Toxo2

наигрался в эту игру

Прошёл, получается, но только на одну концовку :)

anonymous
()
Ответ на: комментарий от Toxo2

Там паралелить в принципе нечего. Можно вынести парсер в отдельный поток, но от этого лучше не станет.

beastie ★★★★★
()
Ответ на: комментарий от no-such-file

PS: другой вариант, парсер параллелить, но не обновлять в нём, а запихивать в канал найденное слово и считать в одной горутине.

Проверил, в наивном подходе более 30s выходит.

beastie ★★★★★
()
Ответ на: комментарий от Toxo2

А, ну для сравнения: c от vel в один поток: real 0m2,309s, с++ от kvpfs в четыре потока: real 0m2,665s

Проблема в том, что железо ныне очень производительное и даже плохие алгоритмы «быстро» работают.
Но такой подход к разработке похож на ком снега, катящийся с горы.
И появляются сотни не эффективных алгоритмов, которые иногда все же умудряются тормозить существующие процессора …
Все же ИМХО нужно стараться вести разработку эффективных алгоритмов /речь о высоко нагруженных функциях/ …

anonymous
()
Ответ на: комментарий от Toxo2

Ну, так хоть в два раза быстрее варианта на php получилось

Принимайте во внимание, что вариант на пхп такой быстрый только потому что он на Си.

fernandos ★★★
()

Мой отчёт :) Си, свой хешмап, один поток.

$ /bin/time -f "Time: %e s; Mem: %M kB" ./count_words
3343241 the
1852717 and
1715705 of
1560152 to
1324244 a
Time: 1.35 s; Mem: 677996 kB

Исходник не прилагаю. Там просто немного оптимизированные под проц функции, ничего интересного.

$ /bin/time -f "Time: %e s; Mem: %M kB" ./vel        
...
Time: 1.67 s; Mem: 439876 kB
anonymous
()
9 февраля 2022 г.

Анонимус вдохновил. Первая версия, код тупо в лоб.

package test;

import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

public class WordsCounter {
    public static void main(String[] args) throws IOException {
        var startTime = System.nanoTime();

        var wordStats = new HashMap<String, WordStat>();

        var inputPath = Path.of("C:\\Users\\Vladimir\\Downloads\\huge.txt");
        try (var fileChannel = FileChannel.open(inputPath)) {
            MappedByteBuffer mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
            var wordBuilder = new StringBuilder();
            while (mappedBuffer.hasRemaining()) {
                byte b = mappedBuffer.get();
                if (('A' <= b) && (b <= 'Z')) {
                    wordBuilder.append((char) (b + ('a' - 'A')));
                } else if (('a' <= b) && (b <= 'z')) {
                    wordBuilder.append((char) b);
                } else {
                    if (!wordBuilder.isEmpty()) {
                        var word = wordBuilder.toString();
                        var wordStat = wordStats.get(word);
                        if (wordStat == null) {
                            wordStat = new WordStat(word);
                            wordStats.put(word, wordStat);
                        } else {
                            wordStat.incCount();
                        }
                        wordBuilder.delete(0, wordBuilder.length());
                    }
                }
            }
        }

        var part1Time = System.nanoTime();
        System.out.println("Part 1: " + (part1Time - startTime));

        var wordStatList = new ArrayList<>(wordStats.values());
        wordStatList.sort(Comparator.comparing(WordStat::count).reversed());

        var part2Time = System.nanoTime();
        System.out.println("Part 2: " + (part2Time - part1Time));

        var resultPath = Path.of("tmp\\result.txt");
        try (var writer = Files.newBufferedWriter(resultPath)) {
            for (WordStat wordStat : wordStatList) {
                writer.write(Integer.toString(wordStat.count()));
                writer.write(' ');
                writer.write(wordStat.word());
                writer.newLine();
            }
        }
        var part3Time = System.nanoTime();
        System.out.println("Part 3: " + (part3Time - part2Time));
        System.out.println("Total: " + (part3Time - startTime));
    }

    static class WordStat {
        private final String word;
        private int count;

        WordStat(String word) {
            this.word = word;
            count = 1;
        }

        String word() {
            return word;
        }

        int count() {
            return count;
        }

        void incCount() {
            count++;
        }
    }
}

Отрабатывает за

Part 1: 4861061700
Part 2: 62542400
Part 3: 59626800
Total: 4983230900

Ну т.е. за 5 секунд примерно.

Сейчас попробую все 4 ядра загрузить. Думаю, секунду-другую есть шансы выиграть.

Legioner ★★★★★
()
Последнее исправление: Legioner (всего исправлений: 1)
Ответ на: комментарий от Legioner

А кстати неплохо получилось.

package test;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class WordsCounter {
    public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
        var startTime = System.nanoTime();
        System.out.println("start");

        List<HashMap<String, WordStat>> wordStatsList = new ArrayList<>();

        var inputPath = Path.of("C:\\Users\\Vladimir\\Downloads\\huge.txt");
        try (var fileChannel = FileChannel.open(inputPath)) {
            MappedByteBuffer mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
            int limit = mappedBuffer.limit();
            var nThreads = Runtime.getRuntime().availableProcessors();
            var executorService = Executors.newFixedThreadPool(nThreads);
            List<Future<HashMap<String, WordStat>>> futures = new ArrayList<>();
            int start = 0;
            for (int i = 1; i < nThreads; i++) {
                int end = i * limit / nThreads;
                while (true) {
                    int b1 = mappedBuffer.get(end - 1);
                    int b2 = mappedBuffer.get(end);
                    boolean l1 = (('A' <= b1) && (b1 <= 'Z')) || (('a' <= b1) && (b1 <= 'z'));
                    boolean l2 = (('A' <= b2) && (b2 <= 'Z')) || (('a' <= b2) && (b2 <= 'z'));
                    if (!l1 || !l2) {
                        break;
                    }
                    end++;
                }
                futures.add(executorService.submit(new CounterTask(mappedBuffer, start, end)));
                start = end;
            }
            futures.add(executorService.submit(new CounterTask(mappedBuffer, start, limit)));

            for (Future<HashMap<String, WordStat>> future : futures) {
                var wordStats = future.get();
                wordStatsList.add(wordStats);
            }
            executorService.shutdown();
        }

        HashMap<String, WordStat> wordStats = wordStatsList.get(0);
        for (int i = 1; i < wordStatsList.size(); i++) {
            var wordStats2 = wordStatsList.get(i);
            for (Map.Entry<String, WordStat> wordStat2Entry : wordStats2.entrySet()) {
                String word = wordStat2Entry.getKey();
                WordStat wordStat2 = wordStat2Entry.getValue();
                WordStat wordStat = wordStats.get(word);
                if (wordStat == null) {
                    wordStats.put(word, wordStat2);
                } else {
                    wordStat.incCount(wordStat2.count());
                }
            }
        }

        var wordStatList = new ArrayList<>(wordStats.values());
        wordStatList.sort(Comparator.comparing(WordStat::count).reversed());

        var resultPath = Path.of("tmp\\result.txt");
        try (var writer = Files.newBufferedWriter(resultPath)) {
            for (WordStat wordStat : wordStatList) {
                writer.write(Integer.toString(wordStat.count()));
                writer.write(' ');
                writer.write(wordStat.word());
                writer.newLine();
            }
        }
        var part4Time = System.nanoTime();
        System.out.println("Total: " + (part4Time - startTime));
    }

    static class WordStat {
        private final String word;
        private int count;

        WordStat(String word) {
            this.word = word;
            count = 1;
        }

        String word() {
            return word;
        }

        int count() {
            return count;
        }

        void incCount() {
            count++;
        }

        void incCount(int amount) {
            count += amount;
        }
    }

    private static class CounterTask implements Callable<HashMap<String, WordStat>> {
        private final ByteBuffer byteBuffer;
        private final int start;
        private final int end;

        CounterTask(ByteBuffer byteBuffer, int start, int end) {
            this.byteBuffer = byteBuffer;
            this.start = start;
            this.end = end;
        }

        @Override
        public HashMap<String, WordStat> call() {
            var wordStats = new HashMap<String, WordStat>();
            var wordBuilder = new StringBuilder(70);
            for (int i = start; i < end; i++) {
                byte b = byteBuffer.get(i);
                if (('A' <= b) && (b <= 'Z')) {
                    wordBuilder.append((char) (b + ('a' - 'A')));
                } else if (('a' <= b) && (b <= 'z')) {
                    wordBuilder.append((char) b);
                } else if (!wordBuilder.isEmpty()) {
                    var word = wordBuilder.toString();
                    var wordStat = wordStats.get(word);
                    if (wordStat == null) {
                        wordStat = new WordStat(word);
                        wordStats.put(word, wordStat);
                    } else {
                        wordStat.incCount();
                    }
                    wordBuilder.delete(0, wordBuilder.length());
                }
            }
            if (!wordBuilder.isEmpty()) {
                var word = wordBuilder.toString();
                var wordStat = wordStats.get(word);
                if (wordStat == null) {
                    wordStat = new WordStat(word);
                    wordStats.put(word, wordStat);
                } else {
                    wordStat.incCount();
                }
            }
            return wordStats;
        }
    }
}
start
Total: 2182135900

2.2 секунды на 4 ядрах. Приемлемо, я считаю.

Legioner ★★★★★
()
Ответ на: комментарий от Legioner

за 5 секунд примерно

Неплохо для жабки. А в сравнении с «эталоном» как? А то может ты там на каком-нибудь оверклокнутом core-x на сотнях гигагерц считаешь.

anonymous
()
Ответ на: комментарий от Legioner
int b1 = mappedBuffer.get(end - 1);
int b2 = mappedBuffer.get(end);
boolean l1 = (('A' <= b1) && (b1 <= 'Z')) || (('a' <= b1) && (b1 <= 'z'));
boolean l2 = (('A' <= b2) && (b2 <= 'Z')) || (('a' <= b2) && (b2 <= 'z'));

А для чего ты тут по два символа из буфера берёшь?

anonymous
()
Ответ на: комментарий от anonymous

Ну смысл в том, что я делю исходный буфер на 4 куска. И мне важно, чтобы граница не разрывала слово. Поэтому если я попал в середину слова, я двигаю индекс, пока не уйду от него.

Legioner ★★★★★
()
Ответ на: комментарий от Legioner

Это понятно. А для чего по два символа сравнивать? Можно же просто искать первый небуквенный, один, и на нём разграничивать куски. Типа

while (true) {
    int b = mappedBuffer.get(end) | 0x20;
    if (b < 'a' || b > 'z')
        break;
    end++;
}
anonymous
()
Ответ на: комментарий от anonymous

Ну если end приходится прямо на начало следующего слова, это тоже нормальная ситуация. Можно и так, как ты написал.

Legioner ★★★★★
()

А это, нормализация не нужна часом? Если да, то задача опять сводится к «у кого в стандартной либе лучше юникод поддерживается».

sislochka
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.