C#:如何使Atkin筛增量化

3

我不确定这是否可能,但我还是想问一下。我的数学和算法技能在这里有些力不从心:P

事情是这样的,我现在有一个类,可以生成小于某个限制值的质数:

public class Atkin : IEnumerable<ulong>
{
    private readonly List<ulong> primes;
    private readonly ulong limit;

    public Atkin(ulong limit)
    {
        this.limit = limit;
        primes = new List<ulong>();
    }

    private void FindPrimes()
    {
        var isPrime = new bool[limit + 1];
        var sqrt = Math.Sqrt(limit);

        for (ulong x = 1; x <= sqrt; x++)
            for (ulong y = 1; y <= sqrt; y++)
            {
                var n = 4*x*x + y*y;
                if (n <= limit && (n % 12 == 1 || n % 12 == 5))
                    isPrime[n] ^= true;

                n = 3*x*x + y*y;
                if (n <= limit && n % 12 == 7)
                    isPrime[n] ^= true;

                n = 3*x*x - y*y;
                if (x > y && n <= limit && n % 12 == 11)
                    isPrime[n] ^= true;
            }

        for (ulong n = 5; n <= sqrt; n++)
            if (isPrime[n])
            {
                var s = n * n;
                for (ulong k = s; k <= limit; k += s)
                    isPrime[k] = false;
            }

        primes.Add(2);
        primes.Add(3);
        for (ulong n = 5; n <= limit; n++)
            if (isPrime[n])
                primes.Add(n);
    }


    public IEnumerator<ulong> GetEnumerator()
    {
        if (!primes.Any())
            FindPrimes();

        foreach (var p in primes)
            yield return p;
    }


    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

现在,我想要做的是消除限制,使得这个序列永远不会停止(理论上)。
我想可以这样做:
1. 从一些微不足道的限制开始 - 找到所有小于该限制的质数 - 产生所有新发现的质数 - 增加限制(通过将旧限制加倍或平方等方式) - 转到步骤2 2. 最好的情况是,步骤二只需要在旧限制和新限制之间工作。换句话说,它不应该再次找到最低的质数。
有没有办法可以做到这一点?我的主要问题是我不太理解算法中的“x”和“y”。例如,我能不能使用相同的算法,但将“x”和“y”设置为“oldLimit”(最初为1),并将其运行到“newLimit”?还是怎么做?有没有聪明的人能够对此进行一些解释?
这样做的目的是,我不必设置那个限制。因此,我可以使用Linq,并只取出我需要的任意数量的质数,而不必担心限制是否足够高等问题。

1
你现在使用的算法是维基百科伪代码的克隆版本,虽然它能够工作,但它是 Atkin 筛法的一个可怕实现。Atkin 与 Bernstein 共同撰写了这个筛法的原始论文,Bernstein 在该论文中提供了伪代码实现,并提供了 C/C++ 源代码的获取方式。一个合理的 Atkin 筛法实现要复杂得多,使用格点解决方案来处理二次方程和模数运算以充分利用性能潜力。我建议你阅读他们的论文,并带上模数运算的知识。 - Jim
5个回答

6
这里提供了一种C#中无界素数筛选的解决方案,可使用埃拉托色尼筛法(SoE)或阿特金筛法(SoA)算法实现。然而,我认为优化的SoA解决方案极其复杂,而真正的SoE在不增加太多复杂性的情况下提供了相同的性能。因此,这可能只是一个部分答案,它展示了如何实现更好的SoA算法并使用SoE实现无界序列,但只是暗示了如何组合这些内容以编写一个相当有效的SoA。
请注意,如果只讨论这些想法最快的实现,则可以跳转到本答案底部。
首先,我们应该评论一下这个练习的要点,即产生一个无界质数序列,以便使用IEnumerable扩展方法(例如Take(),TakeWhile(),Where(),Count()等),因为由于添加了方法调用级别,这些方法会减少一些性能,每次调用和返回枚举一个值至少增加28个机器周期,并添加几个方法调用的级别;尽管如此,即使在对枚举结果使用更好的命令式过滤技术以获得更好的速度时,拥有(有效)无限序列仍然很有用。
请注意,在以下更简单的示例中,我将范围限制为无符号32位数字(uint),因为超过该范围基本的SoE或SoA实现并不真正适用于效率,需要切换到“桶”或其他形式的增量筛法以提高效率。但是,对于完全优化的分段筛法(最快的实现),范围增加到64位范围,尽管如写成一篇文章,人们可能不希望筛选超过约100万亿(10的14次方),因为完整范围的运行时间需要数百年。
由于问题选择SoA的原因可能是错误的,首先将试除法(TD)素数筛法误认为是真正的SoE,然后在TD筛法上使用了一个天真的SoA,因此让我们建立真正有界的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; } }

这段代码在i7-2700K(3.5 GHz)上大约16毫秒内计算出200万以内的质数,以及大约67秒内计算出32位数字范围内4294967291以下的203,280,221个质数(需要256兆字节的内存空间)。现在,为了与真正的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; } }

上述代码在相同的计算机上,大约在10毫秒内计算出200万以内的素数,而在约40秒内计算出32位数字范围内的素数。接下来,让我们确定我们在这里编写的SoA版本是否与上述SoE相比在执行速度上有任何好处。以下代码遵循上述SoE的模型,并根据维基百科文章中建议使用单独的循环来调整“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大致相同的性能?”。

现在对于无限质数序列,最简单的方法是定义一个Uint32.MaxValue的顶部数字常量,并消除primesSoE或primesSoA方法中的参数。 这在某种程度上是低效的,因为即使处理的实际素数值很小,筛选仍然在整个数字范围内完成,这使得确定小范围的质数需要比应有的时间更长。 还有其他理由采用分段版本的质数筛子,而不是这种极端的内存使用:首先,使用主要在CPU L1或L2数据缓存中的数据的算法由于更有效的内存访问而处理速度更快,其次,分段允许将工作轻松地分割成可以使用多核处理器后台攻略的页面,从而可以获得与使用核心数成比例的加速效果。

为了提高效率,我们应该选择一个CPU L1或L2缓存大小为至少16千字节的数组大小,以避免在筛选合数时发生缓存抖动,这意味着BitArray的大小可以是这个值的8倍(每字节8位),即128千位。 由于我们只需要筛选奇素数,因此这表示数字范围超过25万,这意味着分段版本仅使用八个段来筛选到Euler Problem 10所需的200万个数字。 可以保存最后一段的结果并继续进行,但这将排除将此代码适应多核情况,在多个线程上将整理工作委托给后台以充分利用多核处理器的可能性。 下面是(单线程)无限SoE的C#代码:

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毫秒来筛选出所有小于200万的质数,需要大约30秒来筛选完整的32位数字范围。这个代码比使用阶乘轮但未分割的相似版本更快,因为即使代码在计算方面更加复杂,但在不耗费CPU缓存时间时可以节省很多时间。许多复杂性在于每个基础素数每个段的新起始索引的计算,可以通过保存每个素数每个段的状态来减少这些复杂性,但以上代码可以轻松转换为在单独的后台线程上运行筛选过程以进一步提高4倍速度的机器(四核)。如果不使用BitArray类并通过位掩码寻址单个位位置将提供另外一倍左右的速度提升,并且进一步增加阶乘轮会使时间再减少约三分之二。更好地打包用阶乘轮分解消除的值的位数组将稍微提高大范围的效率,但也会在位操作的复杂性上增加一些。然而,所有这些SoE优化都无法达到Berstein SoA实现的极端复杂性,但其运行速度大约相同。
要将以上代码从SoE转换为SoA,我们只需要从有界情况转换到SoA筛选代码,但修改的是每个页面段的起始地址计算方式,类似于SoE情况下计算起始地址,但操作上更加复杂,因为二次方程的提前是通过数字的平方而不是通过简单的素数倍数进行的。我将把所需的适应性留给学生,尽管我真的看不到这个意义,因为经过合理优化的SoA并不比当前版本的SoE更快,并且复杂性要大得多;)。
编辑添加:
注意:以下代码已经被纠正,因为原始发布的代码虽然对于提供的质数序列是正确的,但它需要比必须剔除所有小于范围平方根的数字而只剔除到范围平方根的找到的基本质数慢约三分之一。
在IT技术方面,最有效和简单的优化措施是将每个段页的筛选操作转移到后台线程中。如果有足够的CPU核心,则在这种情况下,枚举素数的主要限制是枚举循环本身,此时,在上述参考机器上(使用C#),可以在约十秒钟内枚举出32位数字范围内的所有素数,而不需要其他优化措施。包括实现IEnumerable接口而不是使用内置迭代器的所有其他优化都可以将其减少到“32位数字范围内的203,280,221个素数大约需要六秒”(10亿个素数则需1.65秒),同样,大部分时间都花在枚举结果上。以下代码包含了许多这些修改,包括使用从Tasks使用的Dotnet Framework 4线程池中的后台任务,使用作为查找表实现的状态机来实现进一步的位打包以使素数枚举更快,并将算法重写为可枚举类,使用“自己编写”的迭代器以增加效率:
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(); } }

请注意,对于小范围的质数(例如到一两百万),此代码并不比上一个版本更快,因为需要设置和初始化数组,但是对于大范围(四十亿及以上)来说,它要快得多。它比Atkin筛法的实现快约8倍,但对于大范围(四十亿及以上)来说,速度可提高20至50倍。代码中定义了常量,设置了基本缓存大小和每个线程要清除的数量(CHNKSZ),这些可能会略微调整性能。还可以尝试进行一些轻微的优化,例如进一步的位打包(如2、3、5、7轮而不仅仅是2、3、5轮),以减少大质数的执行时间,从而将打包减少约25%(可能将执行时间缩短到三分之二),并且通过轮阶乘预先清除页面段,直到17的因子为止,可以进一步减少约20%的时间,但这已经是纯C#代码所能做的极限,与C代码相比,C代码可以利用独特的本机代码优化。

无论如何,如果打算使用IEnumberable接口输出结果,就没有必要进行进一步的优化,因为大约三分之二的时间只用于枚举找到的质数,只有约三分之一的时间用于清除合数。更好的方法是编写类上的方法来实现所需的结果,例如CountTo、ElementAt、SumTo等,以完全避免枚举。

但我确实进行了额外的优化作为额外的答案,尽管增加了复杂性,但仍然显示出额外的好处,并进一步说明为什么不要使用SoA,因为完全优化的SoE实际上更好。


4
下面的代码按照我之前的回答底部所讨论的进行优化,并包含以下功能:
  1. The usable range has been increased to the 64-but unsigned number range of 18,446,744,073,709,551,615 with the range overflow checks removed since it is unlikely that one would want to run the program for the hundreds of years it would take to process the full range of numbers to that limit. This is at very little cost in processing time as the paging can be done using 32-bit page ranges and only the final prime output needs to be computed as a 64-bit number.
  2. It has increased the wheel factorization from a 2,3,5 wheel to use a 2,3,5,7 prime factor wheel with an additional pre-cull of composite numbers using the the additional primes of 11, 13, and 17, to greatly reduce the redundant composite number culling (now only culling each composite number an average of about 1.5 times). Due to the (DotNet related) computational overheads of doing this (also applies for the 2,3,5 wheel as the previous version) the actual time saving in culling isn't all that great but enumerating the answers is somewhat faster due to many of the "simple" composite numbers being skipped in the packed bit representation.
  3. It still uses the Task Parallel Library (TPL) from DotNet 4 and up for multi-threading from the thread pool on a per page basis.
  4. It now uses a base primes representation that supports automatically growing the array contained by this class as more base primes are required as a thread safe method instead of the fixed pre-computed base primes array used previously.
  5. The base primes representation has been reduced to one byte per base prime for a further reduction in memory footprint; thus, the total memory footprint other than the code is the array to hold this base primes representation for the primes up to the square root of the current range being processed, and the packed bit page buffers which are currently set at under the L2 cache size of 256 Kilobytes (smallest page size of 14,586 bytes times the CHNKSZ of 17 as supplied) each per CPU core plus one extra buffer for the foreground task to process. With this code, about three Megabytes is sufficient to process the prime range up to ten to the fourteenth power. As well as speed due to allowing efficient multiprocessing, this reduce memory requirement is the other advantage of using a paged sieve implementation.

    class UltimatePrimesSoE : IEnumerable<ulong> {
      static readonly uint NUMPRCSPCS = (uint)Environment.ProcessorCount + 1; const uint CHNKSZ = 17;
      const int L1CACHEPOW = 14, L1CACHESZ = (1 << L1CACHEPOW), MXPGSZ = L1CACHESZ / 2; //for buffer ushort[]
      //the 2,3,57 factorial wheel increment pattern, (sum) 48 elements long, starting at prime 19 position
      static readonly byte[] WHLPTRN = { 2,3,1,3,2,1,2,3,3,1,3,2,1,3,2,3,4,2,1,2,1,2,4,3,
                                                                               2,3,1,2,3,1,3,3,2,1,2,3,1,3,2,1,2,1,5,1,5,1,2,1 }; const uint FSTCP = 11;
      static readonly byte[] WHLPOS; static readonly byte[] WHLNDX; //to look up wheel indices from position index
      static readonly byte[] WHLRNDUP; //to look up wheel rounded up index positon values, allow for overfolw
      static readonly uint WCRC = WHLPTRN.Aggregate(0u, (acc, n) => acc + n);
      static readonly uint WHTS = (uint)WHLPTRN.Length; static readonly uint WPC = WHTS >> 4;
      static readonly byte[] BWHLPRMS = { 2, 3, 5, 7, 11, 13, 17 }; const uint FSTBP = 19;
      static readonly uint BWHLWRDS = BWHLPRMS.Aggregate(1u, (acc, p) => acc * p) / 2 / WCRC * WHTS / 16;
      static readonly uint PGSZ = MXPGSZ / BWHLWRDS * BWHLWRDS; static readonly uint PGRNG = PGSZ * 16 / WHTS * WCRC;
      static readonly uint BFSZ = CHNKSZ * PGSZ, BFRNG = CHNKSZ * PGRNG; //number of uints even number of caches in chunk
      static readonly ushort[] MCPY; //a Master Copy page used to hold the lower base primes preculled version of the page
      struct Wst { public ushort msk; public byte mlt; public byte xtr; public ushort nxt; }
      static readonly byte[] PRLUT; /*Wheel Index Look Up Table */ static readonly Wst[] WSLUT; //Wheel State Look Up Table
      static readonly byte[] CLUT; // a Counting Look Up Table for very fast counting of primes
      static int count(uint bitlim, ushort[] buf) { //very fast counting
        if (bitlim < BFRNG) { var addr = (bitlim - 1) / WCRC; var bit = WHLNDX[bitlim - addr * WCRC] - 1; addr *= WPC;
          for (var i = 0; i < 3; ++i) buf[addr++] |= (ushort)((unchecked((ulong)-2) << bit) >> (i << 4)); }
        var acc = 0; for (uint i = 0, w = 0; i < bitlim; i += WCRC)
          acc += CLUT[buf[w++]] + CLUT[buf[w++]] + CLUT[buf[w++]]; return acc; }
      static void cull(ulong lwi, ushort[] b) { ulong nlwi = lwi;
        for (var i = 0u; i < b.Length; nlwi += PGRNG, i += PGSZ) MCPY.CopyTo(b, i); //copy preculled lower base primes.
        for (uint i = 0, pd = 0; ; ++i) { pd += (uint)baseprms[i] >> 6;
          var wi = baseprms[i] & 0x3Fu; var wp = (uint)WHLPOS[wi]; var p = pd * WCRC + PRLUT[wi];
          var pp = (p - FSTBP) >> 1; var k = (ulong)p * (pp + ((FSTBP - 1) >> 1)) + pp;
          if (k >= nlwi) break; if (k < lwi) { k = (lwi - k) % (WCRC * p);
            if (k != 0) { var nwp = wp + (uint)((k + p - 1) / p); k = (WHLRNDUP[nwp] - wp) * p - k;
              if (nwp >= WCRC) wp = 0; else wp = nwp; } }
          else k -= lwi; var kd = k / WCRC; var kn = WHLNDX[k - kd * WCRC];
          for (uint wrd = (uint)kd * WPC + (uint)(kn >> 4), ndx = wi * WHTS + kn; wrd < b.Length; ) {
            var st = WSLUT[ndx]; b[wrd] |= st.msk; wrd += st.mlt * pd + st.xtr; ndx = st.nxt; } } }
      static Task cullbf(ulong lwi, ushort[] b, Action<ushort[]> f) {
        return Task.Factory.StartNew(() => { cull(lwi, b); f(b); }); }
      class Bpa {   //very efficient auto-resizing thread-safe read-only indexer class to hold the base primes array
        byte[] sa = new byte[0]; uint lwi = 0, lpd = 0; object lck = new object();
        public uint this[uint i] { get { if (i >= this.sa.Length) lock (this.lck) {
                var lngth = this.sa.Length; while (i >= lngth) {
                  var bf = (ushort[])MCPY.Clone(); if (lngth == 0) {
                    for (uint bi = 0, wi = 0, w = 0, msk = 0x8000, v = 0; w < bf.Length;
                        bi += WHLPTRN[wi++], wi = (wi >= WHTS) ? 0 : wi) {
                      if (msk >= 0x8000) { msk = 1; v = bf[w++]; } else msk <<= 1;
                      if ((v & msk) == 0) { var p = FSTBP + (bi + bi); var k = (p * p - FSTBP) >> 1;
                        if (k >= PGRNG) break; var pd = p / WCRC; var kd = k / WCRC; var kn = WHLNDX[k - kd * WCRC];
                        for (uint wrd = kd * WPC + (uint)(kn >> 4), ndx = wi * WHTS + kn; wrd < bf.Length; ) {
                          var st = WSLUT[ndx]; bf[wrd] |= st.msk; wrd += st.mlt * pd + st.xtr; ndx = st.nxt; } } } }
                  else { this.lwi += PGRNG; cull(this.lwi, bf); }
                  var c = count(PGRNG, bf); var na = new byte[lngth + c]; sa.CopyTo(na, 0);
                  for (uint p = FSTBP + (this.lwi << 1), wi = 0, w = 0, msk = 0x8000, v = 0;
                      lngth < na.Length; p += (uint)(WHLPTRN[wi++] << 1), wi = (wi >= WHTS) ? 0 : wi) {
                    if (msk >= 0x8000) { msk = 1; v = bf[w++]; } else msk <<= 1; if ((v & msk) == 0) {
                      var pd = p / WCRC; na[lngth++] = (byte)(((pd - this.lpd) << 6) + wi); this.lpd = pd; }
                  } this.sa = na; } } return this.sa[i]; } } }
      static readonly Bpa baseprms = new Bpa();
      static UltimatePrimesSoE() {
        WHLPOS = new byte[WHLPTRN.Length + 1]; //to look up wheel position index from wheel index
        for (byte i = 0, acc = 0; i < WHLPTRN.Length; ++i) { acc += WHLPTRN[i]; WHLPOS[i + 1] = acc; }
        WHLNDX = new byte[WCRC + 1]; for (byte i = 1; i < WHLPOS.Length; ++i) {
          for (byte j = (byte)(WHLPOS[i - 1] + 1); j <= WHLPOS[i]; ++j) WHLNDX[j] = i; }
        WHLRNDUP = new byte[WCRC * 2]; for (byte i = 1; i < WHLRNDUP.Length; ++i) {
          if (i > WCRC) WHLRNDUP[i] = (byte)(WCRC + WHLPOS[WHLNDX[i - WCRC]]); else WHLRNDUP[i] = WHLPOS[WHLNDX[i]]; }
        Func<ushort, int> nmbts = (v) => { var acc = 0; while (v != 0) { acc += (int)v & 1; v >>= 1; } return acc; };
        CLUT = new byte[1 << 16]; for (var i = 0; i < CLUT.Length; ++i) CLUT[i] = (byte)nmbts((ushort)(i ^ -1));
        PRLUT = new byte[WHTS]; for (var i = 0; i < PRLUT.Length; ++i) {
          var t = (uint)(WHLPOS[i] * 2) + FSTBP; if (t >= WCRC) t -= WCRC; if (t >= WCRC) t -= WCRC; PRLUT[i] = (byte)t; }
        WSLUT = new Wst[WHTS * WHTS]; for (var x = 0u; x < WHTS; ++x) {
          var p = FSTBP + 2u * WHLPOS[x]; var pr = p % WCRC;
          for (uint y = 0, pos = (p * p - FSTBP) / 2; y < WHTS; ++y) {
            var m = WHLPTRN[(x + y) % WHTS];
            pos %= WCRC; var posn = WHLNDX[pos]; pos += m * pr; var nposd = pos / WCRC; var nposn = WHLNDX[pos - nposd * WCRC];
            WSLUT[x * WHTS + posn] = new Wst { msk = (ushort)(1 << (int)(posn & 0xF)), mlt = (byte)(m * WPC),
                                               xtr = (byte)(WPC * nposd + (nposn >> 4) - (posn >> 4)),
                                               nxt = (ushort)(WHTS * x + nposn) }; } }
        MCPY = new ushort[PGSZ]; foreach (var lp in BWHLPRMS.SkipWhile(p => p < FSTCP)) { var p = (uint)lp;
          var k = (p * p - FSTBP) >> 1; var pd = p / WCRC; var kd = k / WCRC; var kn = WHLNDX[k - kd * WCRC];
          for (uint w = kd * WPC + (uint)(kn >> 4), ndx = WHLNDX[(2 * WCRC + p - FSTBP) / 2] * WHTS + kn; w < MCPY.Length; ) {
            var st = WSLUT[ndx]; MCPY[w] |= st.msk; w += st.mlt * pd + st.xtr; ndx = st.nxt; } } }
      struct PrcsSpc { public Task tsk; public ushort[] buf; }
      class nmrtr : IEnumerator<ulong>, IEnumerator, IDisposable {
        PrcsSpc[] ps = new PrcsSpc[NUMPRCSPCS]; ushort[] buf;
        public nmrtr() { for (var s = 0u; s < NUMPRCSPCS; ++s) ps[s] = new PrcsSpc { buf = new ushort[BFSZ] };
          for (var s = 1u; s < NUMPRCSPCS; ++s) {
            ps[s].tsk = cullbf((s - 1u) * BFRNG, ps[s].buf, (bfr) => { }); } buf = ps[0].buf; }
        ulong _curr, i = (ulong)-WHLPTRN[WHTS - 1]; int b = -BWHLPRMS.Length - 1; uint wi = WHTS - 1; ushort v, msk = 0;
        public ulong Current { get { return this._curr; } } object IEnumerator.Current { get { return this._curr; } }
        public bool MoveNext() {
          if (b < 0) { if (b == -1) b += buf.Length; //no yield!!! so automatically comes around again
            else { this._curr = (ulong)BWHLPRMS[BWHLPRMS.Length + (++b)]; return true; } }
          do {
            i += WHLPTRN[wi++]; if (wi >= WHTS) wi = 0; if ((this.msk <<= 1) == 0) {
              if (++b >= BFSZ) { b = 0; for (var prc = 0; prc < NUMPRCSPCS - 1; ++prc) ps[prc] = ps[prc + 1];
                ps[NUMPRCSPCS - 1u].buf = buf;
                ps[NUMPRCSPCS - 1u].tsk = cullbf(i + (NUMPRCSPCS - 1u) * BFRNG, buf, (bfr) => { });
                ps[0].tsk.Wait(); buf = ps[0].buf; } v = buf[b]; this.msk = 1; } }
          while ((v & msk) != 0u); _curr = FSTBP + i + i; return true; }
        public void Reset() { throw new Exception("Primes enumeration reset not implemented!!!"); }
        public void Dispose() { } }
      public IEnumerator<ulong> GetEnumerator() { return new nmrtr(); }
      IEnumerator IEnumerable.GetEnumerator() { return new nmrtr(); }
      static void IterateTo(ulong top_number, Action<ulong, uint, ushort[]> actn) {
        PrcsSpc[] ps = new PrcsSpc[NUMPRCSPCS]; for (var s = 0u; s < NUMPRCSPCS; ++s) ps[s] = new PrcsSpc {
          buf = new ushort[BFSZ], tsk = Task.Factory.StartNew(() => { }) };
        var topndx = (top_number - FSTBP) >> 1; for (ulong ndx = 0; ndx <= topndx; ) {
          ps[0].tsk.Wait(); var buf = ps[0].buf; for (var s = 0u; s < NUMPRCSPCS - 1; ++s) ps[s] = ps[s + 1];
          var lowi = ndx; var nxtndx = ndx + BFRNG; var lim = topndx < nxtndx ? (uint)(topndx - ndx + 1) : BFRNG;
          ps[NUMPRCSPCS - 1] = new PrcsSpc { buf = buf, tsk = cullbf(ndx, buf, (b) => actn(lowi, lim, b)) };
          ndx = nxtndx; } for (var s = 0u; s < NUMPRCSPCS; ++s) ps[s].tsk.Wait(); }
      public static long CountTo(ulong top_number) {
        if (top_number < FSTBP) return BWHLPRMS.TakeWhile(p => p <= top_number).Count();
        var cnt = (long)BWHLPRMS.Length;
        IterateTo(top_number, (lowi, lim, b) => { Interlocked.Add(ref cnt, count(lim, b)); }); return cnt; }
      public static ulong SumTo(uint top_number) {
        if (top_number < FSTBP) return (ulong)BWHLPRMS.TakeWhile(p => p <= top_number).Aggregate(0u, (acc, p) => acc += p);
        var sum = (long)BWHLPRMS.Aggregate(0u, (acc, p) => acc += p);
        Func<ulong, uint, ushort[], long> sumbf = (lowi, bitlim, buf) => {
          var acc = 0L; for (uint i = 0, wi = 0, msk = 0x8000, w = 0, v = 0; i < bitlim;
              i += WHLPTRN[wi++], wi = wi >= WHTS ? 0 : wi) {
            if (msk >= 0x8000) { msk = 1; v = buf[w++]; } else msk <<= 1;
            if ((v & msk) == 0) acc += (long)(FSTBP + ((lowi + i) << 1)); } return acc; };
        IterateTo(top_number, (pos, lim, b) => { Interlocked.Add(ref sum, sumbf(pos, lim, b)); }); return (ulong)sum; }
      static void IterateUntil(Func<ulong, ushort[], bool> prdct) {
        PrcsSpc[] ps = new PrcsSpc[NUMPRCSPCS];
        for (var s = 0u; s < NUMPRCSPCS; ++s) { var buf = new ushort[BFSZ];
          ps[s] = new PrcsSpc { buf = buf, tsk = cullbf(s * BFRNG, buf, (bfr) => { }) }; }
        for (var ndx = 0UL; ; ndx += BFRNG) {
          ps[0].tsk.Wait(); var buf = ps[0].buf; var lowi = ndx; if (prdct(lowi, buf)) break;
          for (var s = 0u; s < NUMPRCSPCS - 1; ++s) ps[s] = ps[s + 1];
          ps[NUMPRCSPCS - 1] = new PrcsSpc { buf = buf,
                                             tsk = cullbf(ndx + NUMPRCSPCS * BFRNG, buf, (bfr) => { }) }; } }
      public static ulong ElementAt(long n) {
        if (n < BWHLPRMS.Length) return (ulong)BWHLPRMS.ElementAt((int)n);
        long cnt = BWHLPRMS.Length; var ndx = 0UL; var cycl = 0u; var bit = 0u; IterateUntil((lwi, bfr) => {
          var c = count(BFRNG, bfr); if ((cnt += c) < n) return false; ndx = lwi; cnt -= c; c = 0;
          do { var w = cycl++ * WPC; c = CLUT[bfr[w++]] + CLUT[bfr[w++]] + CLUT[bfr[w]]; cnt += c; } while (cnt < n);
          cnt -= c; var y = (--cycl) * WPC; ulong v = ((ulong)bfr[y + 2] << 32) + ((ulong)bfr[y + 1] << 16) + bfr[y];
          do { if ((v & (1UL << ((int)bit++))) == 0) ++cnt; } while (cnt <= n); --bit; return true;
        }); return FSTBP + ((ndx + cycl * WCRC + WHLPOS[bit]) << 1); } }
    
上述代码用了约59毫秒来找到前两百万个质数(由于初始化开销,略慢于其他一些更简单的代码),但分别在1.55秒和5.95秒内计算出十亿和完整数字范围内的质数。这与上一个版本相比速度并没有快多少,因为DotNet多余的枚举已发现质数时的额外数组边界检查所带来的开销超过了筛选合数的时间开销,而后者不到枚举时间的三分之一,因此在枚举中节省筛选合数的时间被额外的时间(每个质数候选人需要进行一次额外的数组边界检查)抵消了。然而,对于许多涉及质数的任务,我们不需要枚举所有质数,而可以直接计算答案而无需枚举。
出于以上原因,该类提供了示例静态方法“CountTo”、“SumTo”和“ElementAt”,以计算或求和给定上限内的质数,或输出基于零的第n个质数。其中,“CountTo”方法将在约0.32秒和1.29秒内分别生成32位数字范围内的十亿个质数的数量;“ElementAt”方法将在约0.32秒和1.25秒内分别生成这些范围内的最后一个元素;而“SumTo”方法将分别在约0.49秒和1.98秒内生成这些范围内所有质数的总和。与欧拉问题10中将所有质数相加到200万的许多朴素实现相比,该程序可以在更短的时间内计算出四十亿个及以上的所有质数的总和,超过了实际范围的2000倍!
这段代码的速度只比primesieve使用的高度优化的C代码慢大约四倍,其速度较慢的原因主要是由于DotNet,具体如下(讨论256千字节缓存的情况,这是L2缓存的大小):
  1. 大部分的执行时间都花费在主要的复合裁剪循环中,这是私有静态"cull"方法中的最后一个"for循环",每个循环只包含四个语句以及范围检查。
  2. 在DotNet中,这个编译需要约21.83个CPU时钟周期,包括每个循环约5个时钟周期的两个数组边界检查。
  3. 非常高效的C编译器将此循环转换为仅约8.33个时钟周期,优势约为2.67倍。
  4. Primesieve还使用极端手动的"循环展开"优化,将每个循环执行所需的平均时间减少到约4.17个时钟周期,额外获得了两倍的收益和总收益约为5.3倍。
  5. 现在,高度优化的C代码既不使用Hyper Thread(HT),也不使用效率较低的Just In Time(JIT)编译器生成的代码,而且primesieve使用的OpemMP多线程似乎没有像在此问题中使用线程池线程那样适应,因此最终的多线程收益只有约四倍。
  6. 可以考虑使用"unsafe"指针来消除数组边界检查,但尝试过后发现JIT编译器无法像正常的基于数组的代码一样优化指针,因此没有边界检查的收益被不那么高效的代码所抵消(每个指针访问都会从内存中重新加载指针地址,而不是像优化的数组情况下使用已经指向该地址的寄存器)。
  7. 当使用可用L1缓存的大小(i3/i5/i7 CPU的16千字节)时,Primesieve甚至可以更快,因为其更有效的代码具有更大的优势,将平均内存访问时间从约四个时钟周期降低到一个时钟周期,这种优势对于DotNet代码来说不那么重要,因为它更多地受益于减少每个页面的处理量。因此,当每个人都使用他们最有效的缓冲区大小时,primesieve大约快五倍。
这段DotNet代码将在大约一个半小时内计算(CountTo)到十万亿的质数数量(已经测试),并且在一天半左右的时间内(估计),计算到十万亿(十四次方)的质数数量,而primesieve的计算时间分别为二十分钟和不到四个小时。从历史上看,这很有趣,因为直到1985年,只有在十三次方范围内的质数计数是已知的,因为在当时昂贵的超级计算机上找到十倍于此范围的质数需要太多执行时间;现在我们可以轻松地在普通台式电脑上(在这种情况下,是Intel i7-2700K - 3.5 GHz)计算这些范围内的质数数量!使用这段代码,很容易理解为什么Atkin教授和Bernstein认为SoA比SoE更快——这是一个持续到今天的神话,原因如下:
  1. 很容易让任何SoA实现计算状态切换的数量和平方自由合数剔除的数量(后者可以使用与SoA算法固有地使用的2,3,5轮优化相同的方式进行优化),以确定32位数字范围内这两个操作的总数约为14亿次。
  2. Bernstein的“等效”SoE实现与他的SoA实现(都不如此代码优化),使用与SoA相同的2,3,5轮优化,将具有大约18.2亿次剔除操作,具有相同的剔除循环计算复杂度。
  3. 因此,Bernstein的结果比他的SoE实现好大约30%左右,仅基于等效操作的数量。但是,他的SoE实现没有将车轮因数分解“发挥到极致”,因为SoA对车轮因数的进一步程度不太敏感,因为2,3,5轮已经“烘焙”到基本算法中。
  4. 这里使用的车轮因数优化将32位数字范围内的合成剔除操作减少到约12亿次;因此,使用此程度的车轮因数的此算法将比等效版本的SoA快约16.7%,因为每个算法的剔除循环可以实现相同。
  5. 使用等效实现编写的代码,对于SoA和primesieve中使用的SoE,代码将响应C编译器优化。
  6. 这种级别的SoE编写比等效SoA更容易,因为只需要一个状态表查找数组即可在基本质数上进行剔除,而不需要为产生有效状态切换的四个二次方程解决方案情况中的每一个额外的状态查找数组。
  7. 如果我通过使用与上述SoE代码相同的技术来实现SoA算法,则生成的SoA仅在枚举输出质数时略微较慢,但在使用静态直接方法时会慢约16.7%
  8. 两个算法的内存占用量没有区别,因为两者都需要表示基本质数和相同数量的段页面缓冲区。
EDIT_ADD: 有趣的是,在x86 32位模式下,此代码运行速度比x64 64位模式快30%,这可能是由于避免将uint32数字扩展为ulong的轻微额外开销所致。以上所有时间都是在64位模式下测量的。END_EDIT_ADD 总之:使用分页Sieve of Atkin实际上比无节约内存要求的最大优化的分页Eratosthenes筛选更慢! 我再说一遍:"为什么要使用Sieve of Atkin?"

2
这是对所提出问题的更明确回答,具体如下:
当然,Atkin筛法(SoA)可以作为分段算法实现,实际上根本不需要使用建议的分段数组,而是可以仅使用原始序列(我已经使用F#函数式地完成了此操作),尽管这比使用可变数组进行剔除所允许的额外效率要慢得多。
我认为它可能是这样的:    1. 从一些微不足道的限制开始    2. 找到所有小于限制的素数    3. 找到所有小于限制的素数    4. 产生所有新发现的质数    5. 增加限制(通过将旧限制翻倍或平方等方法)    6. 转到步骤2
上述算法可以至少以两种不同的方式实现:1)当序列“跑出”当前段并从那些值开始下一段时,保存每个值 'x' 和 'y' 的状态;或者2)计算适用于新段的 'x' 和 'y' 的成对值。虽然第一种方法更简单,但我推荐使用第二种方法,原因有两个:1)它不会使用内存来保存必须保存的所有(许多)x 和 y 的值,只需在“平方自由”筛选步骤中保存基本质数的表示即可,2)它为使用多线程打开了大门,并为每个页面段分配独立的线程操作,在多处理器计算机上节省大量时间。

而且,需要更好地理解 'x' 和 'y':

我的主要问题是我不太理解这个算法中的 x 和 y 是什么。比如,我能否使用相同的算法,但将 x 和 y 设置为 oldLimit(最初为 1),并将其运行到 newLimit?

有一个回答提到了这个问题,但可能不够明确。可以将这些二次方程看作是一个潜在的无限序列,其中'x'或'y'之一从它们的最小值开始固定,另一个变量产生每个序列的所有元素。例如,可以将二次方程的奇数表达式“4*x^2 + y^2”视为以5、17、37、65等开头的序列的序列,并且每个序列都具有{5,13,29,53,...}、{17,25,41,65,...}、{37,45,61,85,...}、{65,73,89,125,...}等元素。显然,其中一些元素不是质数,因为它们是3或5的组合数,这就是为什么需要通过模测试消除它们的原因,或者作为Bernstein实现中的替代方法,可以通过识别生成器的模算术中的模式来自动跳过它们,因此它们实际上甚至不会出现。
实施第一种更简单的生成分段版本SoA的方法只需要保存每个序列的状态,这基本上是在F#增量实现中所做的(尽管为了效率使用了折叠树结构),可以很容易地适应于在数组页面范围内工作。对于每个段页开始计算序列状态的情况,只需要计算每个“活动”序列中有多少元素适合到达新段页中最低元素所表示的数字之前的空间,其中“活动”意味着其起始元素小于段页的起始索引所表示的数字。
至于如何实现基于数组的SoA筛法的分割伪代码,我已经为相关帖子写了一些东西,展示了如何完成此操作。

这样做的目的是我不必设置那个限制。例如,我可以使用Linq并只取出我需要的任意数量的质数,而不必担心限制是否足够高等等。

如另一个答案所述,您可以通过在代码中设置最大“限制”常量来实现此目标,但对于小范围的质数,这将非常低效,因为筛选会在比必要更大的数组上进行。除了提高效率并将内存使用降低到巨大的因素之外,分段还具有其他优点,可以有效地利用多处理。然而,对于大范围的质数,使用Take()、TakeWhile()、Where()、Count()等方法不会提供很好的性能,因为它们的使用涉及每个元素的多个堆叠方法调用和返回的时钟周期。但您可以选择使用这些或更加命令式的程序流程形式,因此这不是一个真正的反对意见。

1

我可以尝试解释一下x和y的作用,但我认为你不能在不从头重新开始循环的情况下完成你的要求。这对于任何“筛法”算法来说都是基本相同的。

筛法的作用基本上是计算每个数字作为解的不同二次方程(偶数或奇数)的数量。检查每个数字的特定方程式取决于n%12是什么。

例如,具有模12余数为1或5的数字n仅当4* x ^ 2 + y ^ 2 = n的解的数量为奇数且该数字为无平方因子时才是质数。第一个循环只是循环遍历可能满足这些不同方程式的x和y的所有可能值。通过每次找到该n的解决方案时翻转isPrime [n],我们可以跟踪解决方案的数量是奇数还是偶数。

问题在于我们同时针对所有可能的n进行计算,这比逐个检查要高效得多。仅针对某些n进行计算需要更长的时间(因为您必须确保n > = lower_limit在第一次循环中),并且会使第二个循环变得更加复杂,因为该循环需要知道所有小于sqrt的质数。

第二个循环检查数字是否是平方自由的(没有质数平方因子)。

此外,我认为您实现的Atkin筛法不一定比直接使用Eratosthenes筛法更快。然而,最快速度最优化的Atkin筛法将击败最快速度最优化的Eratosthenes筛法。


我懂了。嗯,我有一个埃拉托色尼筛法的实现(或者可能只是它的一种变体……突然有些不确定:p),运行得很好,但是它在一段时间后似乎会变得非常慢。我正在使用它们来解决欧拉计划问题,当使用埃拉托色尼算法时,我的第10个问题的解决方案需要大约2秒钟左右,但是使用阿特金算法只需要不到200毫秒。当然,也可能是我的实现完全错误...:p - Svish
Svish,你的Eratosthenes筛法(SoE)可能不是真正的版本,如SoE in F#中所述的问题,而是试除法(TD)(使用所有先前找到的质数的模来确定素性),我的第一个F#程序的C#版本实现了真正的SoE,可以在几十毫秒内计算出Euler Problem 10所需的200万个质数。你的SoA版本比TD好,但不如真正的SoE。 - GordonBGood
@jakber,你在最后一段中的结论应该小心,因为Bernstein的高度优化的Atkin筛(SoE)在特定的筛选范围内击败了同样优化的Eratosthenes筛;然而,SoA本质上具有轮因数分解,消除了2、3和5的因子,而SoE不受此限制,可以包括更高阶的因数分解轮消除 - 使用最大轮因数分解的高度优化的SoE仍然能够击败SoA,例如Kim Walisch的primesieve。 - GordonBGood
@jakber,你的第一句话也不正确,分段筛法是可行的。当然,只要最后一段的参数要么被保存以供下一段使用,就像问题所建议的那样,要么为每个新段计算。保存参数相对容易,例如在随着需求增加而增长的列表中;计算每个新段页面的参数相对更困难,但并非不可能。计算这些参数的优点是可以运行多线程的剪枝部分算法。 - GordonBGood

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接