Параллельное программирование для многоядерных процессоров

Решето Эратосфена для нахождения простых чисел

Разбить на страницы
Показывать лекцию целиком

Рассмотрим задачу отыскания всех простых чисел в интервале от 2 до некоторого заданного числа $$n$$. Выделим в исходном списке $$2,3, \dots , n$$ первое число, которое будет простым, и затем зачеркнем как само число 2, так и все числа в списке, кратные ему. Чтобы зачеркнуть эти кратные числа, достаточно зачеркнуть каждое второе число в списке, начиная с числа 2. Затем выделяем первое незачеркнутое число в качестве очередного простого - им будет число 3, и проводим аналогичную процедуру, зачеркивая каждое третье число, начиная с числа 3. Продолжаем эту процедуру до тех пор, пока не дойдем до очередного простого числа $$p$$, такого что $$p^2 \ge n$$. Легко видеть, что все оставшиеся незачеркнутыми числа в интервале $$p, \dots, n$$ являются простыми. Описанный метод нахождения простых чисел носит название решета Эратосфена.

В этом разделе будет описан параллельный вариант алгоритма решета Эратосфена (http://software.intel.com/en-us/articles/parallel-reduce/ ), взятый из примеров к библиотеке Intel Threading Building Blocks ( http://www.threadingbuildingblocks.org/), и реализованный с использованием средств библиотеки PFX.

Параллельный алгоритм поиска простых чисел на основе решета Эратосфена

Будем рассматривать задачу нахождения количества простых чисел в интервале от 2 до $$n$$ ( $$n \ge 2$$ ). (В действительности, в программе будет иметься специальный ключ, задание которого будет приводить также к распечатке всех найденных простых чисел).

Идея алгоритма состоит в разбиении поиска на 2 этапа: на 1-ом этапе, который выполняется последовательно, находятся все простые числа в диапазоне $$2\dots\sqrt{n} $$ с помощью классического метода решета Эратосфена. Найденные простые числа, на 2-ом этапе, позволяют вычеркнуть все составные числа в диапазоне $$\sqrt{n}\dots n $$. Параллелизация заключается в том, что диапазон $$\sqrt{n}\dots n $$ разбивается на поддиапазоны размера $$m$$, где $$m=round.up.to.even.(\lfloor\sqrt{n}\rfloor)$$, в которых поиск простых чисел может происходить независимо.

В приведенном ниже алгоритме учитываются также следующие свойства задачи:

  • поскольку все четные числа, за исключением числа 2, являются составными, то в каждом поддиапазоне размера $$m$$ рассматриваются только нечетные числа; следствием этого является то, что размер соответствующих массивов равен $$m/2$$ ;
  • в силу выбора размера диапазона равным $$m$$, количество поддиапазонов, рассматриваемых на 2-ом этапе, также не будет превышать $$m$$, что, одновременно, является верхней границей максимального числа потоков для обработки.
  • Описание 1-ой стадии

    Назначение первой стадии ? отыскать все простые числа в поддиапазоне $$2\dots\sqrt{n} $$, которые затем будут использоваться для нахождения простых чисел в следующих поддиапазонах.

    В начале 1-ой стадии, размещаются массивы is_composite и prime_steps размерности $$m/2$$, где первый массив служит для процедуры просеивания, а во втором массиве сохраняются найденный на этой стадии простые числа. В массиве is-composite элемент с номером $$i$$ соответствует нечетному числу $$2_i+1$$. Сам алгоритм состоит в обычном просеивании Эратосфена, начиная с элемента массива is_composite с номером $$i=1$$, т.е., простого числа 3 (простое число 2 учитывается специальным образом).

    Описание 2-ой стадии

    Вторая стадия алгоритма заключается в одновременной обработке $$m$$ поддиапазонов, укладывающихся в интервале $$\sqrt{n}\dots n $$, с помощью нескольких потоков.

    Каждый поток, в общем случае, обрабатывает несколько поддиапазонов, сохраняя количество найденных в них простых чисел в соответствующем элементе массива count_primes. После окончания работы всех потоков (т.е., после выхода из цикла Parallel.For), находится сумма всех элементов массива count_primes, которая добавляется к количеству найденных простых чисел на первом этапе. Это значение - totalCountPrimes и становится результатом решения задачи.

    Ключевым моментом 2-ой стадии является обработка потоком одного поддиапазона, начинающегося с числа start, и имеющего размер $$m$$. Для начала непосредственного просеивания в этом поддиапазоне, необходимо для каждого простого числа $$f$$, найденного на первой стадии, вычислить смещение от начала данного поддиапазона ? т.е., позицию, начиная с которой будет происходить зачеркивание чисел с определенным шагом, равным $$f$$.

    Вычисление данного смещения содержательно можно разбить на 4 шага:

  • определяем число, на котором "остановился" процесс зачеркивания в предыдущем поддиапазоне для данного $$f$$ ; это число определяется по формуле$$k = ( start - 1 ) / f * f$$
  • так как все поддиапазоны, в том числе и предыдущий, имеют размер $$m $$, то от начала этого поддиапазона число $$k $$ отстоит на расстоянии$$p = k \% m$$
  • поскольку в каждом поддиапазоне проверяются на простоту только нечетные числа, то позиция (отсчитываемая от начала предыдущего поддиапазона) следующего нечетного числа (т.е., числа с которого начнется просеивание в текущем поддиапазоне) определяется как

    $$l = p + f,$$ если $$p$$ четно,

    и $$l = p + 2*f$$, если $$p$$ нечетно

  • наконец, определяем позицию относительно данного поддиапазона полученного на предыдущем шаге нечетного числа, учитывая, что в поддиапазоне рассматриваются только нечетные числа:$$fpos = l / 2 - m / 2$$
  • Рассмотрим пример вычисления смещения в соответствии с шагами, представленными выше.

    Пусть $$n = 100$$, тогда $$m = 10$$, и простыми числами, найденными в первом поддиапазоне, являются 3,5,7.

    Рассмотрим, допустим, третий по счету поддиапазон, задаваемый, соответственно параметрами $$start = 20$$ и $$f = 3$$. Тогда$$k = ( 20 -1 ) / 3 * 3 = 18$$

    Т.е., последнее число, кратное 3, в предыдущем поддиапазоне есть число 18, которое отстоит от начала предыдущего поддиапазона на расстоянии

    $$p = 18 % 10 = 8$$

    Т.к. $$p$$ четно, то позиция следующего нечетного числа определяется как

    $$l = 8 + 3 = 11$$

    (легко заметить, что эта позиция соответствует числу 21). Тогда, число 21, с которого мы начнем зачеркивание чисел в третьем поддиапазоне с шагом 3, имеет смещение относительно начала этого диапазона

    $$fpos = 11 / 2 - 10 / 2 = 0$$

    Действительно, число 21 является первым нечетным числом в 3-ем поддиапазоне, а потому оно имеет смещение 0 от начала этого поддиапазона.

    11.2 Реализация с использованием PFX

    Обязательными входными параметрами программы являются: $$n$$ - верхняя граница диапазона, в котором ищутся простые числа; $$p$$ - количество потоков. Необязательным третьим параметром является ключ $$verb$$, задающим печать всех найденных простых чисел. Базовой функцией программы является функция ParallelCountPrimes, входными параметрами которой являются $$n$$ и $$p$$, а возвращаемым значением - количество простых чисел в интервале $$2 \dots n$$.

    Полный текст программы с комментариями приведен ниже.

    //Программа реализует алгоритм поиска простых чисел, подобный алгоримту, 
    //реализованному в примерах библиотеки Intel TBB (tbb21_20080605).
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading;
    
    namespace Sieve
    {
        class Program
        {
            static bool PrintPrimes;
    
            class Multiples
            {
                // хранит флаг простоты нечетного числа в данном поддиапазоне
                private bool[] is_composite;
                // простые числа найденные на первом этапе алгоритма
                private int[] prime_steps;
                //индекс в поддиапазоне is_composite, с которого нужно начать вычеркивание с шагом prime_steps[k]
                private int[] to_strike;
                // число найденных на первом этапе простых чисел
                public int n_prime_steps;
                public int m;
    
                // Конструктор копирования
                public Multiples(Multiples rhs)
                {
                    prime_steps = new int[rhs.prime_steps.Length];
                    rhs.prime_steps.CopyTo(prime_steps, 0);
                    n_prime_steps = rhs.n_prime_steps;
                    m = rhs.m;
                }
    
                public Multiples(int n)
                {
                    m = (int)Math.Sqrt(n);
                    m += m1;
    
                    // Для работы алгоритма требуется меньшее или равное m/2 = (|_sqrt(n)_|)/2 количество ячеек в массиве
                    // так как: 
                    // 1) ищутся только простые числа, меньшие либо равные sqrt(n)
                    // 2) простых чисел не может быть больше половины всех чисел (т.к. каждое второе число четное)
                    is_composite = new bool[m/2];
                    prime_steps = new int[m/2];
                    n_prime_steps = 0;
                    
                    // найдем все простые числа меньшие m: m=to_even(sqrt(n))
                    // перебираем все нечетные числа в поддиапазоне [3;m-1]
                    // (т.к. четные числа автоматически вычеркнуты)
                    for (int i = 3; i<= m; i += 2) 
                    {
                        //если число ранее не было признано составным (т.е. это число простое)
                        if( !is_composite[i/2] ) 
                        {
                            if( PrintPrimes ) Console.WriteLine(i);
    
                            // с шагом равному простому числу перемещаемся по интервалу [простое число/2; m/2]
                            // в результате вычеркнем все числа, делящиеся на простое число i (модифицируем массив is_composite)
                            strike( i/2, m/2, i );
                            // запомним шаг\новое простое число
                            prime_steps[n_prime_steps++] = i;
                        }
                    }       
                }
    
                // Процедура strike вычеркивает составные числа в интервале [start;limit) с шагом step.
                // Возвращает значение limit%step==0? limit: limit-step.
                int strike(int start, int limit, int step)
                {
                    for (; start<= limit; start += step)
                        is_composite[start] = true;
                    return start;
                }
    
                // Поиск простых чисел в пддиапазоне [start,window_size).
                 Возвращает число найденных простых чисел
                // В процессе работы изменяет массив to_strike
                public int find_primes_in_window( int start, int window_size ) 
                {
                    for( uint k=0; kle;n_prime_steps; ++k )
                        to_strike[k] = strike( to_strike[k]-m/2, window_size/2, prime_steps[k] );
    
                    int count = 0;
                    for( int k=0; kle;window_size/2; ++k ) 
                    {
                        if (!is_composite[k]) 
                        {
                            if( PrintPrimes ) Console.WriteLine(start+2*k+1);
                            //нашли простое число, увеличим счетчик на 1
                            count=count+1;
                        }
                    }   
                    return count;
                }
    
                // Функция расчета смещений для каждого шага (простого числа). 
                Смещения указываются от начала поддиапазона 
                public void initialize(int start)
                {
                    is_composite = new bool[m / 2];
                    to_strike = new int[m / 2];
                    for (uint k = 0; k<= n_prime_steps; ++k)
                    {
                        // выберем простое число f (шаг)
                        int f = prime_steps[k];
                        // p - расстояние от начала предыдущего поддиапазона,
                         на котором остановилось вычеркивание с данным шагом f
                        int p = (start - 1) / f * f % m;
    
                        to_strike[k] = (Convert.ToBoolean(p 1) - p + 2 * f : p + f) / 2;
                    }
                }
            }
    
            class Sieve
            {
                public Multiples multiples;
    
                // Число простых чисел, найденных данным решетом
                public int count = 0;
    
                // Конструктор копирования
                public Sieve(Sieve rhs)
                {
                    multiples = new Multiples(rhs.multiples);
                }
    
                public Sieve( int n )
                {
                    multiples = new Multiples(n);
                }
    
                // Поиск простых чисел в подиапазоне [start;start+window_size]
                public int calc(int start, int window_size, int n)
                {
                    // вычисляем начальные смещения для данного поддиапазона (для всех шагов)
                    multiples.initialize(start);
                    int tt = window_size;
                    // контролируем выход за границы общего диапазона
                    if (start + window_size > n)
                        tt = n - start;
    
                    // ищем простые числа в данном поддиапазоне
                    return multiples.find_primes_in_window(start, tt);
                }
    
                // Поиск простых чисел в интервале для данного потока
                public int ThreadSieving(int window_size, int start, int fin, int n)
                {
                    Sieve s1 = new Sieve(this);
                    int count_primes = 0;
    
                    for (int j = start; j<= fin; j += window_size)
                        count_primes += s1.calc(j, window_size, n);
    
                    return count_primes;
                }
            }
    
            public static int ParallelCountPrimes(int n, ref int p)
            {
                // учтем число 2
                totalCountPrimes++;
    
                if( n>=3 ) 
                {
                    // создание решета включает в себя первый этап алгоритма - поиск шагов
                    Sieve s = new Sieve(n);
                    
                    // размер поддиапазона
                    int window_size = s.multiples.m;
                    // запомним количество простых чисел, найденных на первом этапе
                    totalCountPrimes=totalCountPrimes+s.multiples.n_prime_steps;
    
                    int threads = p;
    
                    if(p>window_size)
                    {
                        Console.WriteLine("Warning: Threads number ( "+p+" ) can't be greater
                         then Window Size ( "+window_size+" );");
                        p=window_size;
                        threads = p;
                        Console.WriteLine("Warning: Threads number set to "+window_size+" ;");
                    }
    
                    
                    // массив с числом простых чисел, найденных каждым потоком
                    int[] count_primes = new int[threads];
    
                    
                    // целое количество поддиапазонов в рассматриваемом диапазоне [2..n)
                    // первый поддиапазон был обработан при создании решета s
                    int windows_n = (n  / window_size)-1;
                    // остаток элементов
                    int modn = n % window_size;
                    // целое количество поддиапазонов, приходящихся на один поток
                    int q = windows_n / threads;
                    // оставшиеся число поддиапазонов
                    int r = windows_n % threads; 
    
                    // второй этап алгоритма - параллельный поиск простых чисел по поддиапазонам
                    Parallel.For(0, threads, i =>
                    {
                        // начало интервала (интервал состоит из нескольких поддиапазонов)
                        int start;
                        // конец интервала 
                        int fin;
                        if (i<= r)
                        {
                            start = i * (q + 1) + 1;
                            fin = start + q + 1;
                        }
                        else
                        {
                            start = (r * (q + 1) + (i - r) * q) + 1;
                            fin = start + q;
                        }
    
                        if (i != threads - 1)
                            count_primes[i] = s.ThreadSieving(window_size, start * window_size, fin * window_size, n);
                        else
                            count_primes[i] = s.ThreadSieving(window_size, start * window_size, fin * window_size + modn, n);
                    });
    
                    // определим общее количество найденных простых чисел
                    for (int i = 0; i<= threads; i++)
                        totalCountPrimes += count_primes[i];
                }
                return totalCountPrimes;
            }
    
            // общее число найденных простых чисел в интервале [2..n)
            static int totalCountPrimes = 0;
    
            static void Main(string[] args)
            {
                // кол-во чисел
                int n = 0;
                // число потоков; число потоков не может быть больше window_size
                int p = 0;
    
                try
                {
                    if (args.Length >== 2)
                    {
                        // алгоритм расчитан на работу с открытым справа интервалом [2...n)
                        n = Convert.ToInt32(args[0]);
                        p = Convert.ToInt32(args[1]);
    
                        if (args.Length >== 3)
                            PrintPrimes = args[2] == "verb";
                    }
                    else { Console.WriteLine(Usage()); return; }
                }
                catch (Exception e) { Console.WriteLine(Usage()); return; }
    
                Console.WriteLine("#primes from [2.." + n + ") = " + totalCountPrimes + 
                 " (" + (dt2 - dt1).TotalSeconds + " sec with " + p + "-threads)");
            }
            static string Usage()
            {
                string pname = System.Reflection.Assembly.GetEntryAssembly().GetName().Name;
                StringBuilder s = new StringBuilder();
                for (int i = 0; i<= Console.WindowWidth; i++) s.Append("-");
                return (s.ToString() + Environment.NewLine +
                      "Usage: " + pname + " n p [verb]" + Environment.NewLine +
                      "where" + Environment.NewLine +
                      " n -  upper excluded bound of interval [2...n)" + Environment.NewLine +
                      " p -  threads number" + Environment.NewLine +
                     " verb - verbosity" + Environment.NewLine +
                     s.ToString() + Environment.NewLine);
            }
    
        }
    }
    Страницы:

    Рассмотрим задачу отыскания всех простых чисел в интервале от 2 до некоторого заданного числа $$n$$. Выделим в исходном списке $$2,3, \dots , n$$ первое число, которое будет простым, и затем зачеркнем как само число 2, так и все числа в списке, кратные ему. Чтобы зачеркнуть эти кратные числа, достаточно зачеркнуть каждое второе число в списке, начиная с числа 2. Затем выделяем первое незачеркнутое число в качестве очередного простого - им будет число 3, и проводим аналогичную процедуру, зачеркивая каждое третье число, начиная с числа 3. Продолжаем эту процедуру до тех пор, пока не дойдем до очередного простого числа $$p$$, такого что $$p^2 \ge n$$. Легко видеть, что все оставшиеся незачеркнутыми числа в интервале $$p, \dots, n$$ являются простыми. Описанный метод нахождения простых чисел носит название решета Эратосфена.

    В этом разделе будет описан параллельный вариант алгоритма решета Эратосфена (http://software.intel.com/en-us/articles/parallel-reduce/ ), взятый из примеров к библиотеке Intel Threading Building Blocks ( http://www.threadingbuildingblocks.org/), и реализованный с использованием средств библиотеки PFX.

    Параллельный алгоритм поиска простых чисел на основе решета Эратосфена

    Будем рассматривать задачу нахождения количества простых чисел в интервале от 2 до $$n$$ ( $$n \ge 2$$ ). (В действительности, в программе будет иметься специальный ключ, задание которого будет приводить также к распечатке всех найденных простых чисел).

    Идея алгоритма состоит в разбиении поиска на 2 этапа: на 1-ом этапе, который выполняется последовательно, находятся все простые числа в диапазоне $$2\dots\sqrt{n} $$ с помощью классического метода решета Эратосфена. Найденные простые числа, на 2-ом этапе, позволяют вычеркнуть все составные числа в диапазоне $$\sqrt{n}\dots n $$. Параллелизация заключается в том, что диапазон $$\sqrt{n}\dots n $$ разбивается на поддиапазоны размера $$m$$, где $$m=round.up.to.even.(\lfloor\sqrt{n}\rfloor)$$, в которых поиск простых чисел может происходить независимо.

    В приведенном ниже алгоритме учитываются также следующие свойства задачи:

  • поскольку все четные числа, за исключением числа 2, являются составными, то в каждом поддиапазоне размера $$m$$ рассматриваются только нечетные числа; следствием этого является то, что размер соответствующих массивов равен $$m/2$$ ;
  • в силу выбора размера диапазона равным $$m$$, количество поддиапазонов, рассматриваемых на 2-ом этапе, также не будет превышать $$m$$, что, одновременно, является верхней границей максимального числа потоков для обработки.
  • Описание 1-ой стадии

    Назначение первой стадии ? отыскать все простые числа в поддиапазоне $$2\dots\sqrt{n} $$, которые затем будут использоваться для нахождения простых чисел в следующих поддиапазонах.

    В начале 1-ой стадии, размещаются массивы is_composite и prime_steps размерности $$m/2$$, где первый массив служит для процедуры просеивания, а во втором массиве сохраняются найденный на этой стадии простые числа. В массиве is-composite элемент с номером $$i$$ соответствует нечетному числу $$2_i+1$$. Сам алгоритм состоит в обычном просеивании Эратосфена, начиная с элемента массива is_composite с номером $$i=1$$, т.е., простого числа 3 (простое число 2 учитывается специальным образом).

    Описание 2-ой стадии

    Вторая стадия алгоритма заключается в одновременной обработке $$m$$ поддиапазонов, укладывающихся в интервале $$\sqrt{n}\dots n $$, с помощью нескольких потоков.

    Каждый поток, в общем случае, обрабатывает несколько поддиапазонов, сохраняя количество найденных в них простых чисел в соответствующем элементе массива count_primes. После окончания работы всех потоков (т.е., после выхода из цикла Parallel.For), находится сумма всех элементов массива count_primes, которая добавляется к количеству найденных простых чисел на первом этапе. Это значение - totalCountPrimes и становится результатом решения задачи.

    Ключевым моментом 2-ой стадии является обработка потоком одного поддиапазона, начинающегося с числа start, и имеющего размер $$m$$. Для начала непосредственного просеивания в этом поддиапазоне, необходимо для каждого простого числа $$f$$, найденного на первой стадии, вычислить смещение от начала данного поддиапазона ? т.е., позицию, начиная с которой будет происходить зачеркивание чисел с определенным шагом, равным $$f$$.

    Вычисление данного смещения содержательно можно разбить на 4 шага:

  • определяем число, на котором "остановился" процесс зачеркивания в предыдущем поддиапазоне для данного $$f$$ ; это число определяется по формуле$$k = ( start - 1 ) / f * f$$
  • так как все поддиапазоны, в том числе и предыдущий, имеют размер $$m $$, то от начала этого поддиапазона число $$k $$ отстоит на расстоянии$$p = k \% m$$
  • поскольку в каждом поддиапазоне проверяются на простоту только нечетные числа, то позиция (отсчитываемая от начала предыдущего поддиапазона) следующего нечетного числа (т.е., числа с которого начнется просеивание в текущем поддиапазоне) определяется как

    $$l = p + f,$$ если $$p$$ четно,

    и $$l = p + 2*f$$, если $$p$$ нечетно

  • наконец, определяем позицию относительно данного поддиапазона полученного на предыдущем шаге нечетного числа, учитывая, что в поддиапазоне рассматриваются только нечетные числа:$$fpos = l / 2 - m / 2$$
  • Рассмотрим пример вычисления смещения в соответствии с шагами, представленными выше.

    Пусть $$n = 100$$, тогда $$m = 10$$, и простыми числами, найденными в первом поддиапазоне, являются 3,5,7.

    Рассмотрим, допустим, третий по счету поддиапазон, задаваемый, соответственно параметрами $$start = 20$$ и $$f = 3$$. Тогда$$k = ( 20 -1 ) / 3 * 3 = 18$$

    Т.е., последнее число, кратное 3, в предыдущем поддиапазоне есть число 18, которое отстоит от начала предыдущего поддиапазона на расстоянии

    $$p = 18 % 10 = 8$$

    Т.к. $$p$$ четно, то позиция следующего нечетного числа определяется как

    $$l = 8 + 3 = 11$$

    (легко заметить, что эта позиция соответствует числу 21). Тогда, число 21, с которого мы начнем зачеркивание чисел в третьем поддиапазоне с шагом 3, имеет смещение относительно начала этого диапазона

    $$fpos = 11 / 2 - 10 / 2 = 0$$

    Действительно, число 21 является первым нечетным числом в 3-ем поддиапазоне, а потому оно имеет смещение 0 от начала этого поддиапазона.

    11.2 Реализация с использованием PFX

    Обязательными входными параметрами программы являются: $$n$$ - верхняя граница диапазона, в котором ищутся простые числа; $$p$$ - количество потоков. Необязательным третьим параметром является ключ $$verb$$, задающим печать всех найденных простых чисел. Базовой функцией программы является функция ParallelCountPrimes, входными параметрами которой являются $$n$$ и $$p$$, а возвращаемым значением - количество простых чисел в интервале $$2 \dots n$$.

    Полный текст программы с комментариями приведен ниже.

    //Программа реализует алгоритм поиска простых чисел, подобный алгоримту, 
    //реализованному в примерах библиотеки Intel TBB (tbb21_20080605).
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading;
    
    namespace Sieve
    {
        class Program
        {
            static bool PrintPrimes;
    
            class Multiples
            {
                // хранит флаг простоты нечетного числа в данном поддиапазоне
                private bool[] is_composite;
                // простые числа найденные на первом этапе алгоритма
                private int[] prime_steps;
                //индекс в поддиапазоне is_composite, с которого нужно начать вычеркивание с шагом prime_steps[k]
                private int[] to_strike;
                // число найденных на первом этапе простых чисел
                public int n_prime_steps;
                public int m;
    
                // Конструктор копирования
                public Multiples(Multiples rhs)
                {
                    prime_steps = new int[rhs.prime_steps.Length];
                    rhs.prime_steps.CopyTo(prime_steps, 0);
                    n_prime_steps = rhs.n_prime_steps;
                    m = rhs.m;
                }
    
                public Multiples(int n)
                {
                    m = (int)Math.Sqrt(n);
                    m += m1;
    
                    // Для работы алгоритма требуется меньшее или равное m/2 = (|_sqrt(n)_|)/2 количество ячеек в массиве
                    // так как: 
                    // 1) ищутся только простые числа, меньшие либо равные sqrt(n)
                    // 2) простых чисел не может быть больше половины всех чисел (т.к. каждое второе число четное)
                    is_composite = new bool[m/2];
                    prime_steps = new int[m/2];
                    n_prime_steps = 0;
                    
                    // найдем все простые числа меньшие m: m=to_even(sqrt(n))
                    // перебираем все нечетные числа в поддиапазоне [3;m-1]
                    // (т.к. четные числа автоматически вычеркнуты)
                    for (int i = 3; i<= m; i += 2) 
                    {
                        //если число ранее не было признано составным (т.е. это число простое)
                        if( !is_composite[i/2] ) 
                        {
                            if( PrintPrimes ) Console.WriteLine(i);
    
                            // с шагом равному простому числу перемещаемся по интервалу [простое число/2; m/2]
                            // в результате вычеркнем все числа, делящиеся на простое число i (модифицируем массив is_composite)
                            strike( i/2, m/2, i );
                            // запомним шаг\новое простое число
                            prime_steps[n_prime_steps++] = i;
                        }
                    }       
                }
    
                // Процедура strike вычеркивает составные числа в интервале [start;limit) с шагом step.
                // Возвращает значение limit%step==0? limit: limit-step.
                int strike(int start, int limit, int step)
                {
                    for (; start<= limit; start += step)
                        is_composite[start] = true;
                    return start;
                }
    
                // Поиск простых чисел в пддиапазоне [start,window_size).
                 Возвращает число найденных простых чисел
                // В процессе работы изменяет массив to_strike
                public int find_primes_in_window( int start, int window_size ) 
                {
                    for( uint k=0; kle;n_prime_steps; ++k )
                        to_strike[k] = strike( to_strike[k]-m/2, window_size/2, prime_steps[k] );
    
                    int count = 0;
                    for( int k=0; kle;window_size/2; ++k ) 
                    {
                        if (!is_composite[k]) 
                        {
                            if( PrintPrimes ) Console.WriteLine(start+2*k+1);
                            //нашли простое число, увеличим счетчик на 1
                            count=count+1;
                        }
                    }   
                    return count;
                }
    
                // Функция расчета смещений для каждого шага (простого числа). 
                Смещения указываются от начала поддиапазона 
                public void initialize(int start)
                {
                    is_composite = new bool[m / 2];
                    to_strike = new int[m / 2];
                    for (uint k = 0; k<= n_prime_steps; ++k)
                    {
                        // выберем простое число f (шаг)
                        int f = prime_steps[k];
                        // p - расстояние от начала предыдущего поддиапазона,
                         на котором остановилось вычеркивание с данным шагом f
                        int p = (start - 1) / f * f % m;
    
                        to_strike[k] = (Convert.ToBoolean(p 1) - p + 2 * f : p + f) / 2;
                    }
                }
            }
    
            class Sieve
            {
                public Multiples multiples;
    
                // Число простых чисел, найденных данным решетом
                public int count = 0;
    
                // Конструктор копирования
                public Sieve(Sieve rhs)
                {
                    multiples = new Multiples(rhs.multiples);
                }
    
                public Sieve( int n )
                {
                    multiples = new Multiples(n);
                }
    
                // Поиск простых чисел в подиапазоне [start;start+window_size]
                public int calc(int start, int window_size, int n)
                {
                    // вычисляем начальные смещения для данного поддиапазона (для всех шагов)
                    multiples.initialize(start);
                    int tt = window_size;
                    // контролируем выход за границы общего диапазона
                    if (start + window_size > n)
                        tt = n - start;
    
                    // ищем простые числа в данном поддиапазоне
                    return multiples.find_primes_in_window(start, tt);
                }
    
                // Поиск простых чисел в интервале для данного потока
                public int ThreadSieving(int window_size, int start, int fin, int n)
                {
                    Sieve s1 = new Sieve(this);
                    int count_primes = 0;
    
                    for (int j = start; j<= fin; j += window_size)
                        count_primes += s1.calc(j, window_size, n);
    
                    return count_primes;
                }
            }
    
            public static int ParallelCountPrimes(int n, ref int p)
            {
                // учтем число 2
                totalCountPrimes++;
    
                if( n>=3 ) 
                {
                    // создание решета включает в себя первый этап алгоритма - поиск шагов
                    Sieve s = new Sieve(n);
                    
                    // размер поддиапазона
                    int window_size = s.multiples.m;
                    // запомним количество простых чисел, найденных на первом этапе
                    totalCountPrimes=totalCountPrimes+s.multiples.n_prime_steps;
    
                    int threads = p;
    
                    if(p>window_size)
                    {
                        Console.WriteLine("Warning: Threads number ( "+p+" ) can't be greater
                         then Window Size ( "+window_size+" );");
                        p=window_size;
                        threads = p;
                        Console.WriteLine("Warning: Threads number set to "+window_size+" ;");
                    }
    
                    
                    // массив с числом простых чисел, найденных каждым потоком
                    int[] count_primes = new int[threads];
    
                    
                    // целое количество поддиапазонов в рассматриваемом диапазоне [2..n)
                    // первый поддиапазон был обработан при создании решета s
                    int windows_n = (n  / window_size)-1;
                    // остаток элементов
                    int modn = n % window_size;
                    // целое количество поддиапазонов, приходящихся на один поток
                    int q = windows_n / threads;
                    // оставшиеся число поддиапазонов
                    int r = windows_n % threads; 
    
                    // второй этап алгоритма - параллельный поиск простых чисел по поддиапазонам
                    Parallel.For(0, threads, i =>
                    {
                        // начало интервала (интервал состоит из нескольких поддиапазонов)
                        int start;
                        // конец интервала 
                        int fin;
                        if (i<= r)
                        {
                            start = i * (q + 1) + 1;
                            fin = start + q + 1;
                        }
                        else
                        {
                            start = (r * (q + 1) + (i - r) * q) + 1;
                            fin = start + q;
                        }
    
                        if (i != threads - 1)
                            count_primes[i] = s.ThreadSieving(window_size, start * window_size, fin * window_size, n);
                        else
                            count_primes[i] = s.ThreadSieving(window_size, start * window_size, fin * window_size + modn, n);
                    });
    
                    // определим общее количество найденных простых чисел
                    for (int i = 0; i<= threads; i++)
                        totalCountPrimes += count_primes[i];
                }
                return totalCountPrimes;
            }
    
            // общее число найденных простых чисел в интервале [2..n)
            static int totalCountPrimes = 0;
    
            static void Main(string[] args)
            {
                // кол-во чисел
                int n = 0;
                // число потоков; число потоков не может быть больше window_size
                int p = 0;
    
                try
                {
                    if (args.Length >== 2)
                    {
                        // алгоритм расчитан на работу с открытым справа интервалом [2...n)
                        n = Convert.ToInt32(args[0]);
                        p = Convert.ToInt32(args[1]);
    
                        if (args.Length >== 3)
                            PrintPrimes = args[2] == "verb";
                    }
                    else { Console.WriteLine(Usage()); return; }
                }
                catch (Exception e) { Console.WriteLine(Usage()); return; }
    
                Console.WriteLine("#primes from [2.." + n + ") = " + totalCountPrimes + 
                 " (" + (dt2 - dt1).TotalSeconds + " sec with " + p + "-threads)");
            }
            static string Usage()
            {
                string pname = System.Reflection.Assembly.GetEntryAssembly().GetName().Name;
                StringBuilder s = new StringBuilder();
                for (int i = 0; i<= Console.WindowWidth; i++) s.Append("-");
                return (s.ToString() + Environment.NewLine +
                      "Usage: " + pname + " n p [verb]" + Environment.NewLine +
                      "where" + Environment.NewLine +
                      " n -  upper excluded bound of interval [2...n)" + Environment.NewLine +
                      " p -  threads number" + Environment.NewLine +
                     " verb - verbosity" + Environment.NewLine +
                     s.ToString() + Environment.NewLine);
            }
    
        }
    }
    Вернуться к учебному плану