Ответ 1
Вот решение неограниченного простого просеивания в С#, которое может быть реализовано с использованием алгоритмов Sieve of Eratoshenes (SoE) или Sieve of Atkin (SoA); однако я утверждаю, что вряд ли стоит сложная задача оптимизированного решения SoA, чем истинное SoE дает одинаковую производительность без такой сложности. Таким образом, это, пожалуй, только частичный ответ в этом вопросе, в то время как он показывает, как реализовать лучший алгоритм SoA и показывает, как реализовать неограниченную последовательность с помощью SoE, он только намекает на то, как их объединить для написания разумно эффективного SoA.
Обратите внимание, что если требуется только обсуждение самых быстрых реализаций этих идей, перейдите в нижнюю часть этого ответа.
Сначала мы должны прокомментировать точку этого упражнения при создании неограниченной последовательности простых чисел, чтобы разрешить использование методов расширения IEnumerable, таких как Take(), TakeWhile(), Where(), Count() и т.д., поскольку эти методы выдать некоторую производительность из-за добавленных уровней вызова метода, добавив по меньшей мере 28 машинных циклов для каждого вызова и вернуться, чтобы перечислить одно значение и добавить несколько уровней вызовов методов для каждой функции; что, имея (фактически) бесконечную последовательность, по-прежнему полезно, даже если для получения более высоких скоростей используются более императивные методы фильтрации.
Обратите внимание, что в более простых примерах я ограничил диапазон значений беззнаковых 32-разрядных чисел (uint) как можно дальше от того диапазона, в котором основные реализации SoE или SoA не очень подходят для эффективности и нужны переключиться на "ковш" или другую форму инкрементного сита для части просеивания для повышения эффективности; , однако для полностью оптимизированного сеточного сечения страницы, как и в самой быстрой реализации, диапазон увеличивается до 64-битного диапазона, хотя, как написано, вероятно, не хотелось бы просеивать около ста триллионов (от десяти до четырнадцатой мощности) поскольку время выполнения займет до сотни лет для полного диапазона.
Поскольку вопрос выбирает SoA, вероятно, из-за неправильных причин при первом допущении первичного сита Trial Division (TD) для истинного SoE и затем использования наивного SoA над ситом TD, пусть установить истинное ограниченное SoE, которое может быть реализовано в нескольких строках как метод (который может быть преобразован в класс согласно стилю кодирования вопроса) следующим образом:
static IEnumerable<uint> primesSoE(uint top_number) {
if (top_number < 2u) yield break;
yield return 2u; if (top_number < 3u) yield break;
var BFLMT = (top_number - 3u) / 2u;
var SQRTLMT = ((uint)(Math.Sqrt((double)top_number)) - 3u) / 2u;
var buf = new BitArray((int)BFLMT + 1,true);
for (var i = 0u; i <= BFLMT; ++i) if (buf[(int)i]) {
var p = 3u + i + i; if (i <= SQRTLMT) {
for (var j = (p * p - 3u) / 2u; j <= BFLMT; j += p)
buf[(int)j] = false; } yield return p; } }
Это вычисляет простые числа до 2 миллионов в течение примерно 16 миллисекунд на i7-2700K (3,5 ГГц) и 203 280 221 простых числах до 4,294,967,291 в 32-битном диапазоне номеров примерно за 67 секунд на одном компьютере (с учетом запасных 256 MegaBytes оперативной памяти).
Теперь использование версии выше для сравнения с SoA вряд ли справедливо, так как истинный SoA автоматически игнорирует кратные 2, 3 и 5, поэтому введение факторизации колес, чтобы сделать то же самое для SoE, дает следующий код:
static IEnumerable<uint> primesSoE(uint top_number) {
if (top_number < 2u) yield break;
yield return 2u; if (top_number < 3u) yield break;
yield return 3u; if (top_number < 5u) yield break;
yield return 5u; if (top_number < 7u) yield break;
var BFLMT = (top_number - 7u) / 2u;
var SQRTLMT = ((uint)(Math.Sqrt((double)top_number)) - 7u) / 2u;
var buf = new BitArray((int)BFLMT + 1,true);
byte[] WHLPTRN = { 2, 1, 2, 1, 2, 3, 1, 3 };
for (uint i = 0u, w = 0u; i <= BFLMT; i += WHLPTRN[w], w = (w < 7u) ? ++w : 0u)
if (buf[(int)i]) { var p = 7u + i + i; if (i <= SQRTLMT) {
var pX2 = p + p; uint[] pa = { p, pX2, pX2 + p };
for (uint j = (p * p - 7u) / 2u, m = w; j <= BFLMT;
j += pa[WHLPTRN[m] - 1u], m = (m < 7u) ? ++m : 0u)
buf[(int)j] = false; } yield return p; } }
Вышеприведенный код вычисляет простые числа до 2 миллионов в течение примерно 10 миллисекунд, а числа до 32-разрядного номера - примерно 40 секунд на том же компьютере, что и выше.
Затем определите, может ли версия SoA, которую мы, вероятно, написать здесь, действительно имеет какую-либо выгоду по сравнению с SoE в соответствии с приведенным выше кодом, насколько скорость выполнения идет. Приведенный ниже код следует за моделью SoE выше и оптимизирует псевдокод SoA из статьи в Википедии относительно настройки диапазонов 'x 'и' y ', используя отдельные петли для отдельных квадратичных решений, как предложено в этой статье, решая квадратичные уравнения (и квадратные свободные исключения) только для нечетных решений, объединяя квадратичную форму "3 * x ^ 2" для решения как для положительные и отрицательные второстепенные слагаемые в одном и том же проходе и устраняя дорогостоящие по модулю операции с вычислением, чтобы создать код, который более чем в три раза быстрее, чем наивная версия псевдокода, размещенная там и используемая в этом вопросе. Код ограниченного SoA следующий:
static IEnumerable<uint> primesSoA(uint top_number) {
if (top_number < 2u) yield break;
yield return 2u; if (top_number < 3u) yield break;
yield return 3u; if (top_number < 5u) yield break;
var BFLMT = (top_number - 3u) / 2u; var buf = new BitArray((int)BFLMT + 1, false);
var SQRT = (uint)(Math.Sqrt((double)top_number)); var SQRTLMT = (SQRT - 3u) / 2u;
for (uint x = 1u, s = 1u, mdx12 = 5u, dmdx12 = 0u; s <= BFLMT; ++x, s += ((x << 1) - 1u) << 1) {
for (uint y = 1u, n = s, md12 = mdx12, dmd12 = 8u; n <= BFLMT; y += 2, n += (y - 1u) << 1) {
if ((md12 == 1) || (md12 == 5)) buf[(int)n] = buf[(int)n] ^ true;
md12 += dmd12; if (md12 >= 12) md12 -= 12; dmd12 += 8u; if (dmd12 >= 12u) dmd12 -= 12u; }
mdx12 += dmdx12; if (mdx12 >= 12u) mdx12 -= 12u; dmdx12 += 8u; if (dmdx12 >= 12u) dmdx12 -= 12u; }
for (uint x = 1u, s = 0u, mdx12 = 3u, dmdx12 = 8u; s <= BFLMT; ++x, s += x << 1) {
int y = 1 - (int)x, n = (int)s, md12 = (int)mdx12, dmd12 = ((-y - 1) << 2) % 12;
for (; (y < 0) && (uint)n <= BFLMT; y += 2, n += (-y + 1) << 1) {
if (md12 == 11) buf[(int)n] = buf[(int)n] ^ true;
md12 += dmd12; if (md12 >= 12) md12 -= 12; dmd12 += 4; if (dmd12 >= 12) dmd12 -= 12; }
if (y < 1) { y = 2; n += 2; md12 += 4; dmd12 = 0; } else { n += 1; md12 += 2; dmd12 = 8; }
if (md12 >= 12) md12 -= 12; for (; (uint)n <= BFLMT; y += 2, n += (y - 1) << 1) {
if (md12 == 7) buf[(int)n] = buf[(int)n] ^ true;
md12 += dmd12; if (md12 >= 12) md12 -= 12; dmd12 += 8; if (dmd12 >= 12) dmd12 -= 12; }
mdx12 += dmdx12; if (mdx12 >= 12) mdx12 -= 12; dmdx12 += 4; if (dmdx12 >= 12) dmdx12 -= 12; }
for (var i = 0u; i<=BFLMT; ++i) if (buf[(int)i]) { var p = 3u+i+i; if (i<=SQRTLMT) { var sqr = p*p;
for (var j = (sqr - 3ul) / 2ul; j <= BFLMT; j += sqr) buf[(int)j] = false; } yield return p; } }
Это все еще более чем в два раза медленнее, чем алгоритм SoE с разбивкой колес, который был опубликован из-за не полностью оптимизированного числа операций. Дальнейшая оптимизация может быть выполнена в коде SoA, как при использовании операций по модулю 60, так и в отношении исходного алгоритма (не псевдокода) и использования бит-бит для обработки только кратных, исключая 3 и 5, чтобы получить код, достаточно близкий по производительности к SoE или даже немного превзойти его, но мы все ближе и ближе усложняем реализацию Berstein для достижения этой производительности. Моя точка зрения: "Почему SoA, когда кто-то работает очень сложно, чтобы получить такую же производительность, что и SoE?".
Теперь для последовательности неограниченных простых чисел самый простой способ сделать это - определить const top_number из Uint32.MaxValue и исключить аргумент в методах primesSoE или primesSoA. Это несколько неэффективно в том, что отбраковка все еще выполняется по всему диапазону чисел, независимо от того, обрабатывается фактическое основное значение, что делает определение для небольших диапазонов простых чисел намного длиннее, чем должно было бы. Есть и другие причины перейти к сегментированной версии симулята простых чисел, кроме этого и использования экстремальной памяти. Во-первых, алгоритмы, которые используют данные, которые в основном находятся в кэшах данных L1 или L2 процессора, ускоряются из-за более эффективного доступа к памяти и во-вторых, поскольку сегментация позволяет легко разбивать задание на страницы, которые можно отбирать в фоновом режиме, используя многоядерные процессоры для ускорения, которые могут быть пропорциональны количеству используемых сердечников.
Для эффективности мы должны выбрать размер массива размера кеша процессора L1 или L2, который составляет не менее 16 килобайт для большинства современных процессоров, чтобы избежать обхода кэша, поскольку мы отбираем композиты простых чисел из массива, а это значит, что битаррей может иметь размер в восемь раз больше (8 бит на байт) или 128 килобитов. Поскольку нам нужно только просеивать нечетные простые числа, это представляет собой диапазон чисел более четверти миллиона, а это означает, что сегментированная версия будет использовать только восемь сегментов для решеток до 2 миллионов, как того требует проблема Эйлера 10. Можно было бы сохранить результаты из последний сегмент и продолжить с этой точки, но это не позволит адаптировать этот код к многоядерному случаю, когда один из них отбрасывает фон на несколько потоков, чтобы в полной мере использовать многоядерные процессоры. Код С# для неограниченного SoE (одиночный поток) выглядит следующим образом:
static IEnumerable<uint> primesSoE() { yield return 2u; yield return 3u; yield return 5u;
const uint L1CACHEPOW = 14u + 3u, L1CACHESZ = (1u << (int)L1CACHEPOW); //for 16K in bits...
const uint BUFSZ = L1CACHESZ / 15u * 15u; //an even number of wheel rotations
var buf = new BitArray((int)BUFSZ);
const uint MAXNDX = (uint.MaxValue - 7u) / 2u; //need maximum for number range
var SQRTNDX = ((uint)Math.Sqrt(uint.MaxValue) - 7u) / 2u;
byte[] WHLPTRN = { 2, 1, 2, 1, 2, 3, 1, 3 }; //the 2,3,5 factorial wheel, (sum) 15 elements long
byte[] WHLPOS = { 0, 2, 3, 5, 6, 8, 11, 12 }; //get wheel position from index
byte[] WHLNDX = { 0, 0, 1, 2, 2, 3, 4, 4, 5, 5, 5, 6, 7, 7, 7, //get index from position
0, 0, 1, 2, 2, 3, 4, 4, 5, 5, 5, 6, 7 }; //allow for overflow
byte[] WHLRNDUP = { 0, 2, 2, 3, 5, 5, 6, 8, 8, 11, 11, 11, 12, 15, //allow for overflow...
15, 15, 17, 17, 18, 20, 20, 21, 23, 23, 26, 26, 26, 27 };
uint BPLMT = (ushort.MaxValue - 7u) / 2u; var bpbuf = new BitArray((int)BPLMT + 1, true);
for (var i = 0; i <= 124; ++i) if (bpbuf[i]) { var p = 7 + i + i; //initialize baseprimes array
for (var j = (p * p - 7) / 2; j <= BPLMT; j += p) bpbuf[j] = false; } var pa = new uint[3];
for (uint i = 0u, w = 0, si = 0; i <= MAXNDX;
i += WHLPTRN[w], si += WHLPTRN[w], si = (si >= BUFSZ) ? 0u : si, w = (w < 7u) ? ++w : 0u) {
if (si == 0) { buf.SetAll(true);
for (uint j = 0u, bw = 0u; j <= BPLMT; j += WHLPTRN[bw], bw = (bw < 7u) ? ++bw : 0u)
if (bpbuf[(int)j]) { var p = 7u+j+j; var pX2=p+p; var k = p * (j + 3u) + j;
if (k >= i + BUFSZ) break; pa[0] = p; pa[1] = pX2; pa[2] = pX2 + p; var sw = bw; if (k < i) {
k = (i - k) % (15u * p); if (k != 0) { var os = WHLPOS[bw]; sw = os + ((k + p - 1u) / p);
sw = WHLRNDUP[sw]; k = (sw - os) * p - k; sw = WHLNDX[sw]; } } else k -= i;
for (; k<BUFSZ; k+=pa[WHLPTRN[sw]-1], sw=(sw<7u) ? ++sw : 0u) buf[(int)k]=false; } }
if (buf[(int)si]) yield return 7u + i + i; } }
Приведенный выше код занимает около 16 миллисекунд, чтобы просеять простые числа до двух миллионов и около 30 секунд, чтобы пролить полный 32-разрядный диапазон чисел. Этот код быстрее, чем аналогичная версия, используя факториальное колесо без сегментации для больших диапазонов чисел, потому что, несмотря на то, что код более сложный по отношению к вычислениям, очень много времени сохраняется, не разбирая кэширование CPU. Большая часть сложности заключается в вычислении новых начальных индексов на базовый базис на каждый сегмент, который можно было бы сократить, сохранив состояние каждого в расчете на каждый сегмент, но приведенный выше код можно легко преобразовать, чтобы запустить процессы отбора отдельные потоки фона для дальнейшего четырехкратного ускорения для машины с четырьмя ядрами, еще больше с восемью ядрами. Не использование класса BitArray и обращение к отдельным местам бит с помощью бит-масок обеспечивали бы дополнительную скорость примерно в два раза, а дальнейшее увеличение факториального колеса обеспечило бы еще одно сокращение времени примерно до двух третей. Лучшая упаковка бит-массива в исключенных индексах для значений, исключенных с помощью факторизации колес, немного повысит эффективность для больших диапазонов, но также добавит немного сложности в манипуляции бит. Тем не менее, все эти оптимизации SoE не будут соответствовать экстремальной сложности реализации Berstein SoA, но будут работать с одинаковой скоростью.
Чтобы преобразовать вышеприведенный код из SoE в SoA, нам нужно только изменить код отсечения SoA из ограниченного случая, но с изменением, которое начальные адреса пересчитываются для каждого сегмента страницы, как будто начальный адрес вычисляется для случай SoE, но даже несколько более сложный в работе, поскольку квадратики продвигаются по квадратам чисел, а не простым кратным простых чисел. Я оставлю необходимые адаптации ученику, хотя я действительно не вижу смысла в том, что SoA с разумной оптимизацией не быстрее текущей версии SoE и значительно сложнее;)
EDIT_ADD:
Примечание. Приведенный ниже код был скорректирован, так как в то время как исходный опубликованный код был прав в отношении последовательности заданных простых чисел, он был наполовину медленнее, чем нужно, поскольку он был выбран для всех чисел под квадратным корнем от диапазона, а не только от найденной базы, до квадратного корня диапазона.
Самая эффективная и простая оптимизация - это отключение операций отбраковки на сегментную страницу до фоновых потоков, так что, учитывая достаточное количество ядер процессора, основной предел перечисления простых чисел - это сам цикл перечисления, и в этом случае все простые числа в 32 -битный диапазон номеров можно перечислить примерно на десять секунд на вышеупомянутом эталонном компьютере (на С#) без других оптимизаций, причем все остальные оптимизации включают в себя запись реализаций интерфейса IEnumerable, а не использование встроенных итераторов, уменьшающих это до около шести секунд для всех 203,22,221 простых чисел в 32-битном диапазоне номеров (от 1,65 секунды до одного миллиарда), снова с большей частью времени, затрачиваемым только на перечисление результатов. Следующий код содержит многие из этих изменений, в том числе использование фоновых задач из Dotnet Framework 4 ThreadPool, используемого задачами, с использованием конечного автомата, реализованного в виде таблицы поиска, для реализации дополнительной битовой упаковки, чтобы ускорить перечисление простых чисел и переписать алгоритм как перечислимый класс с использованием итераторов "roll-your-own" для повышения эффективности:
class fastprimesSoE : IEnumerable<uint>, IEnumerable {
struct procspc { public Task tsk; public uint[] buf; }
struct wst { public byte msk; public byte mlt; public byte xtr; public byte nxt; }
static readonly uint NUMPROCS = (uint)Environment.ProcessorCount + 1u; const uint CHNKSZ = 1u;
const uint L1CACHEPOW = 14u, L1CACHESZ = (1u << (int)L1CACHEPOW), PGSZ = L1CACHESZ >> 2; //for 16K in bytes...
const uint BUFSZ = CHNKSZ * PGSZ; //number of uints even number of caches in chunk
const uint BUFSZBTS = 15u * BUFSZ << 2; //even in wheel rotations and uints (and chunks)
static readonly byte[] WHLPTRN = { 2, 1, 2, 1, 2, 3, 1, 3 }; //the 2,3,5 factorial wheel, (sum) 15 elements long
static readonly byte[] WHLPOS = { 0, 2, 3, 5, 6, 8, 11, 12 }; //get wheel position from index
static readonly byte[] WHLNDX = { 0, 1, 1, 2, 3, 3, 4, 5, 5, 6, 6, 6, 7, 0, 0, 0 }; //get index from position
static readonly byte[] WHLRNDUP = { 0, 2, 2, 3, 5, 5, 6, 8, 8, 11, 11, 11, 12, 15, 15, 15, //allow for overflow...
17, 17, 18, 20, 20, 21, 23, 23, 26, 26, 26, 27, 30, 30, 30 }; //round multiplier up
const uint BPLMT = (ushort.MaxValue - 7u) / 2u; const uint BPSZ = BPLMT / 60u + 1u;
static readonly uint[] bpbuf = new uint[BPSZ]; static readonly wst[] WHLST = new wst[64];
static void cullpg(uint i, uint[] b, int strt, int cnt) { Array.Clear(b, strt, cnt);
for (uint j = 0u, wp = 0, bw = 0x31321212u, bi = 0u, v = 0xc0881000u, bm = 1u; j <= BPLMT;
j += bw & 0xF, wp = wp < 12 ? wp += bw & 0xF : 0, bw = (bw > 3u) ? bw >>= 4 : 0x31321212u) {
var p = 7u + j + j; var k = p * (j + 3u) + j; if (k >= (i + (uint)cnt * 60u)) break;
if ((v & bm) == 0u) { if (k < i) { k = (i - k) % (15u * p); if (k != 0) {
var sw = wp + ((k + p - 1u) / p); sw = WHLRNDUP[sw]; k = (sw - wp) * p - k; } }
else k -= i; var pd = p / 15;
for (uint l = k / 15u + (uint)strt * 4u, lw = ((uint)WHLNDX[wp] << 3) + WHLNDX[k % 15u];
l < (uint)(strt + cnt) * 4u; ) { var st = WHLST[lw];
b[l >> 2] |= (uint)st.msk << (int)((l & 3) << 3); l += st.mlt * pd + st.xtr; lw = st.nxt; } }
if ((bm <<= 1) == 0u) { v = bpbuf[++bi]; bm = 1u; } } }
static fastprimesSoE() {
for (var x = 0; x < 8; ++x) { var p = 7 + 2 * WHLPOS[x]; var pr = p % 15;
for (int y = 0, i = ((p * p - 7) / 2); y < 8; ++y) { var m = WHLPTRN[(x + y) % 8]; i %= 15;
var n = WHLNDX[i]; i += m * pr; WHLST[x * 8 + n] = new wst { msk = (byte)(1 << n), mlt = m,
xtr = (byte)(i / 15),
nxt = (byte)(8 * x + WHLNDX[i % 15]) }; }
} cullpg(0u, bpbuf, 0, bpbuf.Length); } //init baseprimes
class nmrtr : IEnumerator<uint>, IEnumerator, IDisposable {
procspc[] ps = new procspc[NUMPROCS]; uint[] buf;
Task dlycullpg(uint i, uint[] buf) { return Task.Factory.StartNew(() => {
for (var c = 0u; c < CHNKSZ; ++c) cullpg(i + c * PGSZ * 60, buf, (int)(c * PGSZ), (int)PGSZ); }); }
public nmrtr() {
for (var i = 0u; i < NUMPROCS; ++i) ps[i] = new procspc { buf = new uint[BUFSZ] };
for (var i = 1u; i < NUMPROCS; ++i) { ps[i].tsk = dlycullpg((i - 1u) * BUFSZBTS, ps[i].buf); } buf = ps[0].buf; }
public uint Current { get { return this._curr; } } object IEnumerator.Current { get { return this._curr; } }
uint _curr; int b = -4; uint i = 0, w = 0; uint v, msk = 0;
public bool MoveNext() {
if (b < 0) if (b == -1) { _curr = 7; b += (int)BUFSZ; }
else { if (b++ == -4) this._curr = 2u; else this._curr = 7u + ((uint)b << 1); return true; }
do { i += w & 0xF; if ((w >>= 4) == 0) w = 0x31321212u; if ((this.msk <<= 1) == 0) {
if (++b >= BUFSZ) { b = 0; for (var prc = 0; prc < NUMPROCS - 1; ++prc) ps[prc] = ps[prc + 1];
ps[NUMPROCS - 1u].buf = buf; var low = i + (NUMPROCS - 1u) * BUFSZBTS;
ps[NUMPROCS - 1u].tsk = dlycullpg(i + (NUMPROCS - 1u) * BUFSZBTS, buf);
ps[0].tsk.Wait(); buf = ps[0].buf; } v = buf[b]; this.msk = 1; } }
while ((v & msk) != 0u); if (_curr > (_curr = 7u + i + i)) return false; else return true; }
public void Reset() { throw new Exception("Primes enumeration reset not implemented!!!"); }
public void Dispose() { }
}
public IEnumerator<uint> GetEnumerator() { return new nmrtr(); }
IEnumerator IEnumerable.GetEnumerator() { return new nmrtr(); } }
Обратите внимание, что этот код не быстрее последней версии для небольших диапазонов простых чисел, как до одного или двух миллионов из-за накладных расходов на настройку и инициализацию массивов, но значительно быстрее для более крупных диапазонов до четырех миллиардов плюс, Это примерно в 8 раз быстрее, чем вопрос реализации сита Аткина, но идет от 20 до 50 раз быстрее для больших диапазонов до четырех миллиардов плюс. Определены константы в коде, устанавливающие размер базового кеша и сколько из них отбираются на поток (CHNKSZ), который может слегка улучшить производительность. Можно было бы попытаться провести некоторые небольшие оптимизации, которые могли бы сократить время выполнения для больших простых чисел до двух раз, таких как дополнительная упаковка бит, как для колеса 2,3,5,7, а не только 2,3,5 колеса для сокращение примерно на 25% в упаковке (возможно, сокращение времени выполнения до двух третей) и предварительное отбраковка сегментов страницы колесиком факториала до коэффициента 17 для дальнейшего уменьшения примерно на 20%, но это примерно так о том, что можно сделать в чистом коде С# по сравнению с C-кодом, который может использовать преимущества уникальной оптимизации собственного кода.
Во всяком случае, вряд ли стоит больше оптимизировать, если намереваться использовать интерфейс IEnumberable для вывода, поскольку вопрос требует, так как около двух третей времени используется только для перечисления найденных простых чисел и только около одной трети времени потраченные на отбраковку составных чисел. Лучшим подходом было бы написать методы в классе для реализации желаемых результатов, таких как CountTo, ElementAt, SumTo и т.д., Чтобы полностью исключить перечисление.
Но я сделал дополнительную оптимизацию как дополнительный ответ , который показывает дополнительные преимущества, несмотря на дополнительную сложность, и который далее показывает, почему никто не делает хотите использовать SoA, потому что полностью оптимизированный SoE на самом деле лучше.