![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Поистине, Go — шикарный язык программирования. У него настолько нет недостатков, что его создатели, не зная, что ещё усовершенствовать, решили сделать для него специальные новые шрифты. А что? Хорошим программам — хорошее оформление:

Давайте, всё же, немножко поменяем программу, и, вместо того, чтобы выводить простые числа, посчитаем их количество. Не всех, конечно, а тех, которые меньше определённого порогового значения. Изменится при этом только функция
Запускаем:
Пробуем ещё:
Ждём.
Ждём.
А, вот:
А главное, программу так легко распараллелить! Просто написали
Покороче, но зато ни разу не распараллеливается, все эти циклы выполняются итерация за итерацией — ужас. Вот, смотрите:
Хотя вообще, понятно, тест нечестный. Там же куча накладных расходов — создание всех этих тредов, каналов, пересылка сообщений через них... Ясно, что на подобном синтетическом примере они будут играть основную роль. Давайте введём их в нашу «классическую» версию: будем создавать по дополнительному потоку на каждый чих (точнее, на каждое простое число), а числа будем не тупо отмечать в массиве, а пересылать в сообщениях. И оставим всё однопоточным: каждый поток, породив ещё один, будет тупо ждать, пока тот завершится:
Тут много чего происходит; в общем, на каждое простое число создаётся даже не один канал, а два (один — в функции
Ладно, хорош издеваться. В Северной Корее запрещён сарказм, но мы-то, слава Монстру, не в Северной Корее (если вы таки в Северной Корее, то могу вас только поздравить с тем, что вам удалось вылезти в интернет). Если коротко, то: нефиг было лекции прогуливать. В варианте от авторов Go каждое простое число мы проверяем на делимость на каждое меньшее простое число. Что всего в два раза меньше, чем если бы мы проверяли каждое простое число на каждое простое число — а это, по сути, все пары простых чисел, что означает (количество простых чисел)2 операций сравнения. Простых чисел, меньших
А вот классическое решето Эратосфена (второй или третий вариант) работает гораздо быстрее: для каждого простого числа
Мораль. Книжки надо читать и мозги развивать, а не шрифты для своего недоязычка программирования придумывать.

Давайте, всё же, немножко поменяем программу, и, вместо того, чтобы выводить простые числа, посчитаем их количество. Не всех, конечно, а тех, которые меньше определённого порогового значения. Изменится при этом только функция
main
, так что никаких проблем быть не должно:package main import "fmt" import "os" import "strconv" func Generate(ch chan<- int) { for i := 2; ; i++ { ch <- i } } func Filter(in <-chan int, out chan<- int, prime int) { for { i := <-in if i%prime != 0 { out <- i } } } func main() { c := 0 n, _ := strconv.Atoi(os.Args[1]) ch := make(chan int) go Generate(ch) prime := <-ch for prime <= n { c++ ch1 := make(chan int) go Filter(ch, ch1, prime) ch = ch1 prime = <-ch } fmt.Println(c) }Уж извините, но я менял программу, и она перестала быть достойной этих роскошных шрифтов.
Запускаем:
$ gccgo -g -o primes primes.go $ ./primes 10 4И правда, простых чисел до 10 всего четыре: 2, 3, 5 и 7. Ура!
Пробуем ещё:
$ ./primes 100 25И правда. Ещё:
$ ./primes 100000Не понял, висим, что ли?
Ждём.
Ждём.
А, вот:
9592Долго что-то. Ну, дело такое, простые числа считаются нелегко. Не то, чтобы для них формула была, правда?
А главное, программу так легко распараллелить! Просто написали
go
— и готово! Программа тривиальным образом делается быстрее во сколько хошь раз! Вот, для сравнения, однопоточная, классическая версия:package main import "fmt" import "os" import "strconv" func main() { n, _ := strconv.Atoi(os.Args[1]) sieve := make([]bool, n-1) c := 0 for p, b := range sieve { if !b { c++ for i := 2; i <= n / (p+2); i++ { sieve[i*(p+2)-2] = true } } } fmt.Println(c) }
Покороче, но зато ни разу не распараллеливается, все эти циклы выполняются итерация за итерацией — ужас. Вот, смотрите:
$ gccgo -g -o slowPrimes slowPrimes.go $ ./slowPrimes 100000 9592Не понял.
$ time ./primes 100000; time ./slowPrimes 100000 9592 real 3m20.962s user 2m36.270s sys 0m44.936s 9592 real 0m0.047s user 0m0.028s sys 0m0.020sОго.
Хотя вообще, понятно, тест нечестный. Там же куча накладных расходов — создание всех этих тредов, каналов, пересылка сообщений через них... Ясно, что на подобном синтетическом примере они будут играть основную роль. Давайте введём их в нашу «классическую» версию: будем создавать по дополнительному потоку на каждый чих (точнее, на каждое простое число), а числа будем не тупо отмечать в массиве, а пересылать в сообщениях. И оставим всё однопоточным: каждый поток, породив ещё один, будет тупо ждать, пока тот завершится:
package main import "fmt" import "os" import "strconv" type SieveCommand interface { perform(sieve []bool, top int) } type Cross struct { number int } func (c Cross) perform(sieve []bool, top int) { sieve[c.number-2] = true } func cross(number int, sieveCommands chan<- SieveCommand) { sieveCommands <- Cross{number: number} } type Lookup struct { from int result chan<- int } func (l Lookup) perform(sieve []bool, top int) { for p := l.from; p <= top; p++ { if !sieve[p-2] { l.result <- p return } } l.result <- 0 } func lookup(from int, sieveCommands chan<- SieveCommand) int { result := make(chan int) sieveCommands <- Lookup{from: from, result: result} return <-result } func Sieve(sieve []bool, top int, commands <-chan SieveCommand) { for { command := <-commands command.perform(sieve, top) } } func OnPrime(prime int, top int, sieveCommands chan<- SieveCommand, result chan<- int) { for n := 2*prime; n <= top; n += prime { cross(n, sieveCommands) } p := lookup(prime+1, sieveCommands) if p == 0 { result <- 1 } else { subResult := make(chan int) go OnPrime(p, top, sieveCommands, subResult) r := <-subResult result <- r+1 } } func main() { n, _ := strconv.Atoi(os.Args[1]) sieve := make([]bool, n-1) sieveCommands := make(chan SieveCommand) go Sieve(sieve, n, sieveCommands) result := make(chan int) go OnPrime(2, n, sieveCommands, result) r := <-result fmt.Println(r) }
Тут много чего происходит; в общем, на каждое простое число создаётся даже не один канал, а два (один — в функции
lookup
, чтобы отыскать следующее, а другой — для передачи результата). Причём, найдя новое простое число, мы создаём новый поток, а старый — замирает, и ждёт, пока ему принесут на блюдечке этот самый результат. Ну, теперь точно будет медленно.$ gccgo -g -o verySlowPrimes Dropbox/verySlowPrimes.go $ time ./verySlowPrimes 100000 9592 real 0m2.186s user 0m1.000s sys 0m1.174sМ-дя. Помедленне, конечно, но как-то не получаются те три минуты, которые были в исходном варианте.
Ладно, хорош издеваться. В Северной Корее запрещён сарказм, но мы-то, слава Монстру, не в Северной Корее (если вы таки в Северной Корее, то могу вас только поздравить с тем, что вам удалось вылезти в интернет). Если коротко, то: нефиг было лекции прогуливать. В варианте от авторов Go каждое простое число мы проверяем на делимость на каждое меньшее простое число. Что всего в два раза меньше, чем если бы мы проверяли каждое простое число на каждое простое число — а это, по сути, все пары простых чисел, что означает (количество простых чисел)2 операций сравнения. Простых чисел, меньших
n
, всего около n / ln n
(гуглить по словам «асимптотика простых чисел»), так что получается n2 / (ln n)2
— почти что квадратичное количество операций.А вот классическое решето Эратосфена (второй или третий вариант) работает гораздо быстрее: для каждого простого числа
p
вычёркиваются примерно n / p
чисел — то есть, всего количество операций будет n * (1/2 + 1/3 + 1/5 + 1/7 + ...)
. Это можно ОЧЕНЬ ГРУБО оценить сверху как n * (1/1 + 1/2 + 1/3 + 1/4 + ... + 1/n)
(просто добавив слагаемых), а это, в свою очередь — как n * (1/1 + (1/2 + 1/2) + (1/4 + 1/4 + 1/4 + 1/4) + ...)
— каждая из внутренних скобок равна единице, а их количество — log2n
, то есть получается максимум n * log2n
операций. На самом деле, оценка слишком грубая, и правильный ответ будет n * ln(ln n)
— но и так хорошо, почти линейная зависимость.Мораль. Книжки надо читать и мозги развивать, а не шрифты для своего недоязычка программирования придумывать.
Запрещённое (в Северной Корее, по крайней мере)
Date: 2016-12-15 03:58 pm (UTC)no subject
Date: 2016-12-15 06:18 pm (UTC)Ты им, кстати, написал, чтобы исправили? Или там некуда писать?
no subject
Date: 2016-12-15 06:45 pm (UTC)