Ниже приводится вопрос для интервью:

Следующая однострочная строка генерирует и отображает список из первых 500 простых чисел. Как бы вы оптимизировали его, используя параллельный LINQ, сохраняя при этом ЕДИНОЕ ЗАЯВЛЕНИЕ C #:

MessageBox.Show(string.Join(",", 
    Enumerable.Range(2, (int)(500 * (Math.Log(500) + Math.Log(System.Math.Log(500)) - 0.5)))
                .Where(x => Enumerable.Range(2, x - 2)
                                      .All(y => x % y != 0))
                .TakeWhile((n, index) => index < 500)));

Я попытался ввести в запрос AsParallel(), а также ParallelEnumerable, но не увидел ощутимых преимуществ на многоядерных машинах. Запрос по-прежнему интенсивно использует одно ядро ​​ЦП, в то время как другие ядра проводят свободное время. Может ли кто-нибудь предложить улучшение, которое равномерно распределяет нагрузку на все ядра и, таким образом, сокращает время выполнения?

Для энтузиастов : следующая формула возвращает верхнюю границу, которая гарантированно будет больше, чем N простых чисел, т.е. если вы проверите до этого числа, вы обязательно найдете N простых чисел меньше его:

UpperBound = N * (Log(N) + Log(Log(N)) - 0.5) //Log is natural log
21
dotNET 11 Дек 2014 в 07:53
 – 
abatishchev
11 Дек 2014 в 08:01
1
@abatishchev: Внесенное вами изменение недействительно. Вопрос явно просит сохранить его в виде одного утверждения. Также это приводит к ошибке времени компиляции. Я верну это. К тому же ссылка, которой вы поделились, отличается по нескольким причинам. Во-первых, он не использует параллелизм. Во-вторых, он показывает простые числа между x и y, тогда как в этом вопросе показаны первые N простых чисел, что является совершенно другой концепцией (поскольку в этом случае у вас нет верхней границы).
 – 
dotNET
11 Дек 2014 в 08:35
@dotNET, к сожалению, у вас все еще есть ошибка в коде после плохой правки абатищева. Отсутствует закрывающая скобка в методе Range.
 – 
Adam Smith
11 Дек 2014 в 08:44
Кроме того, спасибо за объяснение формулы верхней границы, очень круто!
 – 
Adam Smith
11 Дек 2014 в 08:45
1
@AdamSmith: Спасибо, что указали. Исправленный.
 – 
dotNET
11 Дек 2014 в 08:48

4 ответа

Лучший ответ

Это прекрасно работает на моей машине. Я никогда не видел, чтобы все мои ядра работали на 100% до сих пор. Спасибо, что дали мне повод поиграть :)

Я увеличивал числа, пока у меня не было достаточно медленного времени для измерения (20 000).

Ключевым параметром, который изменил меня, была установка ExecutionMode < / a> в ForceParallelism.

Поскольку я использую вариант слияния NotBuffered, я повторно сортирую его, когда закончу. В этом нет необходимости, если вас не волнует порядок результатов (возможно, вы помещаете результаты в HashSet).

DegreeOfParallelism и MergeOptions обеспечили лишь незначительный прирост производительности (если таковой был) на моей машине. В этом примере показано, как использовать все параметры в одном операторе Linq, что и было первоначальным вопросом.

var numbers = Enumerable.Range(2, (int)(20000 * (Math.Log(20000) + Math.Log(System.Math.Log(20000)) - 0.5)))
                .AsParallel()
                .WithDegreeOfParallelism(Environment.ProcessorCount) 
                .WithExecutionMode(ParallelExecutionMode.ForceParallelism)
                .WithMergeOptions(ParallelMergeOptions.NotBuffered) // remove order dependancy
                .Where(x => Enumerable.Range(2, x - 2)
                                      .All(y => x % y != 0))
                .TakeWhile((n, index) => index < 20000);
string result = String.Join(",",numbers.OrderBy (n => n));
18
Mark Peters 11 Дек 2014 в 22:30
Замечательный. Это сокращает время почти вдвое. От 16,1 до 8,5 секунд для 10 000 простых чисел на моем 2-ядерном компьютере. Я вношу небольшое изменение, изменяя 8 на Environment.ProcessorCount, чтобы сделать его общим. Благодаря тонну. Я приму это, если в ближайшее время у нас ничего не получится.
 – 
dotNET
11 Дек 2014 в 09:12
После добавления SQRT во внутренний Enumerable.Range() время сократилось с 8,5 до 0,2 секунды.
 – 
dotNET
11 Дек 2014 в 11:57

Вы можете проверить только SQRT значения, чтобы сделать это (обновленный код сверху)

var numbers = new[] {2, 3}.Union(Enumerable.Range(2, (int) (i*(Math.Log(i) + Math.Log(Math.Log(i)) - 0.5)))
                                           .AsParallel()
                                           .WithDegreeOfParallelism(Environment.ProcessorCount)
                                           // 8 cores on my machine
                                           .WithExecutionMode(ParallelExecutionMode.ForceParallelism)
                                           .WithMergeOptions(ParallelMergeOptions.NotBuffered)
                                           // remove order dependancy
                                           .Where(x => Enumerable.Range(2, (int) Math.Ceiling(Math.Sqrt(x)))
                                                                 .All(y => x%y != 0))
                                           .TakeWhile((n, index) => index < i))
                          .ToList();

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

private static IEnumerable<int> GetPrimes(int k)
{
    int n = (int)Math.Ceiling((k * (Math.Log(k) + Math.Log(Math.Log(k)) - 0.5)));
    bool[] prime = new bool[n + 1];
    prime[0] = prime[1] = false;
    for (int i = 2; i < prime.Length; i++)
    {
        prime[i] = true;
    }
    for (int i = 2; i*i <= n; ++i) // valid for n < 46340^2 = 2147395600
        if (prime[i])
        {
            for (int j = i*i; j <= n; j += i)
                prime[j] = false;
            yield return i;
        }
}

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

7
Alex Zhukovskiy 13 Дек 2014 в 19:54
Спасибо за вклад. +1 за то, что напомнил мне про SQRT. Что касается вашего алгоритма, это же знаменитый алгоритм сита, верно? Я уже знаю об этом, но вопрос, с которым я столкнулся, явно просил сделать это в одном заявлении, что невозможно сделать без LINQ, поэтому речь шла не о моде, это было просто требованием. Спасибо, в любом случае. :)
 – 
dotNET
11 Дек 2014 в 11:40
В качестве меры предосторожности обратите внимание, что Math.Ceiling() вместе с Math.Sqrt() приведет к тому, что 2 и 3 будут рассматриваться как составные, поэтому их следует добавлять вручную в список вывода.
 – 
dotNET
11 Дек 2014 в 12:00
Конечно. См. Метод Union из исходного кода
 – 
Alex Zhukovskiy
11 Дек 2014 в 13:52
Stopwatch t = new Stopwatch();
            t.Start();
            var numbers = Enumerable.Range(2, (int)(500 * (Math.Log(500) + Math.Log(System.Math.Log(500)) - 0.5)))
                .Where(x => Enumerable.Range(2, x - 2)
                                      .All(y => x % y != 0))
                .TakeWhile((n, index) => index < 500);
            t.Stop();
            MessageBox.Show(t.ElapsedMilliseconds.ToString());
            MessageBox.Show(string.Join(",", numbers));

Он оценивается через 3 миллисекунды. Хороший запрос linq.

-5
Mahesh Malpani 11 Дек 2014 в 08:45
1
Где улучшение?
 – 
dotNET
11 Дек 2014 в 08:46
Собственно добавление в комментарии. Есть несколько проблем с синтаксисом. Исправил те
 – 
Mahesh Malpani
11 Дек 2014 в 08:48
12
Вы на самом деле ничего не вычисляете (отсюда 3 мс), LINQ имеет отложенное выполнение.
 – 
Adam Smith
11 Дек 2014 в 08:59
2
Да, 3 миллисекунды для создания объекта запроса, а теперь сколько времени на самом деле получить значения?
 – 
Jon Hanna
13 Дек 2014 в 20:59

Для будущих читателей это то, чем я закончил. Это довольно быстро. На моей скромной машине он генерирует список из первых 20000 простых чисел меньше секунды.

Enumerable.Range(5, (int)(N * (Math.Log(N) + Math.Log(System.Math.Log(N)) - 0.5)))
            .AsParallel()
            .WithDegreeOfParallelism(Environment.ProcessorCount)
            .WithExecutionMode(ParallelExecutionMode.ForceParallelism)
            .WithMergeOptions(ParallelMergeOptions.NotBuffered) // remove order dependancy
            .Where(x => Enumerable.Range(2, (int)Math.Ceiling(Math.Sqrt(x)))
                                  .All(y => x % y != 0))
            .TakeWhile((n, index) => index < N).Concat(new int[] { 2, 3 }.AsParallel()).OrderBy(x => x).Take(N);
2
dotNET 27 Ноя 2017 в 20:25
Вы можете еще больше сократить время на запрос, изменив .TakeWhile((n, index) => index < N) на .TakeWhile((n, index) => index < N-2), а затем удалив .Take(N).
 – 
adityap
24 Авг 2018 в 23:13