【问题标题】:Checksumming large swathes of prime numbers? (for verification)对大量素数进行校验和? (用于验证)
【发布时间】:2014-10-28 10:47:10
【问题描述】:

是否有任何聪明的算法可以计算数百万或数十亿素数的高质量校验和? IE。具有最大的错误检测能力并且可能是可分段的?

动机:

小素数 - 大小高达 64 位 - 可以根据需要以每秒数百万的速度进行筛分,方法是使用一个小位图来筛选潜在因子(最多 2^32-1)和第二个位图来筛选目标范围内的数字。

算法和实现相当简单明了,但魔鬼在细节中:值往往会在任何地方推动 - 或超过 - 内置整数类型的限制,边界情况比比皆是(可以这么说),甚至浮点数的差异如果编程没有适当的防御性,严格性可能会导致破坏。更不用说优化编译器可能造成的混乱,即使是在静态库中已经编译、已经测试的代码上(如果使用链接时代码生成)。更不用说更快的算法往往更复杂,因此更脆弱。

这有两个后果:除非使用最终的可执行映像执行测试,否则测试结果基本上没有意义,并且非常需要在运行时验证正常使用期间的正确操作。

对照预先计算的值进行检查可以提供最高程度的置信度,但所需的文件又大又笨重。具有 1000 万个素数的文本文件有大约 100 MB 未压缩和超过 10 MB 压缩;存储字节编码的差异需要每个素数一个字节,熵编码最多可以将大小减少一半(1000 万个素数需要 5 MB)。因此,即使是一个仅涵盖高达 2^32 的小因素的文件,其重量也会达到 100 MB 左右,并且解码器的复杂性将超过窗口筛本身的复杂性。

这意味着检查文件是不可行的,除非作为对新建可执行文件的最终版本检查。更不用说值得信赖的文件来之不易。 Prime Pages 提供前 5000 万个素数的文件,即使是惊人的 primos.mat.br 也只能达到 1,000,000,000,000。这是不幸的,因为许多边界情况(== 需要测试)发生在 2^62 和 2^64-1 之间。

这会留下校验和。这样,空间需求将是微不足道的,并且仅与测试用例的数量成正比。我不想要求提供像 MD5 或 SHA-256 这样的体面校验和,并且目标数字都是素数,应该可以通过对数字进行一些简单的操作来生成高质量、高分辨率的校验和自己。

这是我到目前为止所想出的。原始摘要由四个 64 位数字组成;最后,它可以折叠到所需的尺寸。

   for (unsigned i = 0; i < ELEMENTS(primes); ++i)
   {
      digest[0] *= primes[i];              // running product (must be initialised to 1)
      digest[1] += digest[0];              // sum of sequence of running products
      digest[2] += primes[i];              // running sum
      digest[3] += digest[2] * primes[i];  // Hornerish sum
   }

每个素数有两个(非相关)muls,速度足够好,除了简单的总和之外,每个组件总是发现所有错误,我试图偷偷溜过摘要。但是,我不是数学家,经验测试并不能保证有效性。

是否有一些数学属性可以用来设计——而不是像我那样“煮”——一个合理、可靠的校验和?

是否可以设计校验和以使其可步进,在某种意义上,子范围可以单独处理,然后将结果与一些算术相结合以给出相同的结果,就好像整个范围已被校验和一样一气呵成?与当今所有高级 CRC 实现趋于具有相同的功能,以启用并行处理。

编辑当前方案的基本原理是:计数、总和和乘积不依赖于将素数添加到摘要中的顺序;它们可以在单独的块上计算,然后组合起来。校验和确实取决于顺序;这就是它存在的理由。但是,如果可以以某种方式组合两个连续块的两个校验和以给出组合块的校验和,那就太好了。

有时可以根据外部来源验证计数和总和,例如oeis.org 上的某些序列,或针对primos.mat.br 上的 1000 万个素数批次等来源(索引给出第一个和最后一个素数,数字 = = 1000 万是隐含的)。但是,产品和校验和没有这样的运气。

在我花费大量时间和计算能力来计算和验证摘要之前,涵盖了高达 2^64 的整个小因子范围,我想听听专家对此的看法......

我目前在 32 位和 64 位变体中测试的方案如下所示:

template<typename word_t>
struct digest_t
{
   word_t count;
   word_t sum;
   word_t product;
   word_t checksum;

   // ...

   void add_prime (word_t n)
   {
      count    += 1;
      sum      += n;
      product  *= n;
      checksum += n * sum + product;
   }
};

这样做的好处是 32 位摘要分量等于相应 64 位值的下半部分,这意味着即使需要快速 32 位验证,也只需要计算存储 64 位摘要。一个 32 位版本的摘要可以在这个简单的sieve test program@pastebin 中找到,用于动手实验。可以在 a sieve that works up to 2^64-1 的更新粘贴中找到修订后的模板版本中的完整 Monty。

【问题讨论】:

  • 前 23,163,298 个素数被认为是压缩友好的。它是每个间隙
  • @vitaly-t:通过注意除了 2 和 3 之间的所有间隙都是偶数,压缩友好领域可以扩展很多。这使您可以覆盖所有小于 303,371,455,241(第一次出现的间隙 > 512)的素数,远远超出 32 位范围。
  • 这是一个很好的收获!我现在可能会修改my implementation for this。
  • 它带来了两个问题:1)您需要将段映射的大小加倍(一个用于按索引快速定位),因为 32 位不再足够 2)额外的位在按索引访问期间进行操作,这需要尽可能快。所以可以进一步压缩,只是不再那么压缩友好了:)
  • @vitaly-t:至少这允许它为 all 32 位素数使用字节间隙,这对于某些应用程序来说是一个巨大的胜利。唯一真正复杂的是对素数 2 的特殊处理;间隙值的缩放要么是完全免费的(当使用索引缩放时),要么非常便宜以至于几乎没有几率(移位指令)。

标签: primes checksum discrete-mathematics


【解决方案1】:

我在Cell 架构上做了很多并行操作。这有类似的感觉。

在这种情况下,我会使用一个快速且可能递增的哈希函数(例如xxHash 或MurmurHash3)和一个hash list(这是Merkle Tree 的一个不太灵活的特化)。

这些哈希值非常快。通过一些简单的操作来变得更好会非常困难。哈希列表提供了并行性——列表的不同块可以由不同的线程处理,然后您对哈希进行哈希处理。您也可以使用 Merkle 树,但我怀疑这会更复杂而没有太多好处。

  • 实际上将您的范围划分为对齐的块——我们将这些称为微块。 (例如,一个微块是一个范围,例如 [n
  • 要处理微块,请计算您需要计算的内容,将其添加到缓冲区中,然后对缓冲区进行哈希处理。 (增量散列函数将提供更小的缓冲区。缓冲区不必每次都填充相同长度的数据。)
  • 每个微块哈希都将放在circular buffer 中。
  • 将循环缓冲区分成可散列的块(“宏块”)。当这些宏块可用或没有更多微块时,以适当的顺序递增地散列这些宏块。
  • 生成的哈希是您想要的。

一些补充说明:

  • 我推荐一种设计,其中线程保留一系列待处理的微块,循环缓冲区有空间,处理它们,转储循环缓冲区中的值,然后重复。
  • 这还有一个额外的好处,就是您可以动态决定要使用多少线程。例如当请求新的微块范围时,每个线程都可以检测是否有太多/太少的线程在运行并进行调整。
  • 我个人会让线程将最后一个微块散列添加到宏块来清理该宏块。以这种方式调整的参数更少。
  • 维护一个循环缓冲区并不像听起来那么难——仍然未处理的最低阶宏块定义了循环缓冲区所代表的“宏块空间”的哪个部分。您只需要一个简单的计数器,它会在适当的时候递增来表达这一点。
  • 另一个好处是,由于线程定期经历一个保留/工作/保留/工作循环,因此速度出乎意料的慢的线程几乎不会严重影响运行时间。
  • 如果您希望做一些不那么健壮但更简单的事情,您可以通过使用“条纹”模式放弃大量工作——确定最大线程数 (N),并让每个线程处理每个第 N 个微块(由其线程“ID”偏移)并散列每个线程的结果宏块。然后在最后,散列来自 N 个线程的宏块散列。如果您的线程数少于 N,您可以将工作分配给您想要的线程数。 (例如,最多 64 个线程,但三个真实线程,线程 0 处理 21 个虚拟线程,线程 1 处理 21 个虚拟线程,线程 2 处理 22 个虚拟线程——不理想,但并不可怕)这本质上是一个浅默克尔树,而不是一个哈希列表。

【讨论】:

  • 诚然,我基本上忽略了您对哈希函数的要求,该哈希函数具有比通常的摘要函数更强大的属性,而是将其换成如何使您选择的任何摘要函数可并行化。但这样做时,您可能会丢弃属性更强的哈希函数的任何有趣属性。
  • 您的回答非常有趣且经过深思熟虑。它并没有完全解决上述问题,因为它需要一些复杂的散列函数,其复杂性约为 MD5 或 SHA-256,并且它没有利用输入的特殊属性(所有素数)。但是,您的解决方案确实很好地解决了一个稍微更普遍的问题,即以可分离(可并行/可步进)的方式散列大量内存,即使底层哈希函数不可步进(与 CRC 不同,您可以使用线性代数来将连续块的结果拼接在一起)
  • 因此,我因您的出色回答而奖励赏金,同时也着眼于这样一个事实,即由该主题的问题吸引到这里的专家可能期望找到类似的东西。我自己也很喜欢 Murmurhash,Merkle 树在很多场合都救了我的命。从某种意义上说,Merkle 树非常适合该问题,因为摘要的一个预期应用是以大致类似于 B-Tree 的方式覆盖范围 0..2^64-1,通过在结果可用时添加结果并且是已验证。那里的对象是允许后续验证而不存储 TB 数据。
【解决方案2】:

Kaganar 的出色回答展示了即使相邻块的摘要无法在数学上组合以给出与组合块已被消化的结果相同的结果,如何使事情正常进行。

他的解决方案的唯一缺点是生成的块结构必然相当僵化,就像PKI 具有官方包罗万象的认证层次结构与“游击式”PGP 的信任网络仅涵盖少数感兴趣的科目。换句话说,它需要设计一个全局寻址结构/层次结构。

这是当前形式的摘要;变化在于顺序相关部分已被简化到其基本最小值:

void add_prime (word_t n)
{
   count    += 1;
   sum      += n;
   product  *= n;
   checksum += n * count;
}

以下是从该摘要的实际工作中获得的经验教训:

  • count、sum 和 product(即部分原始模字大小)被证明非常有用,因为它们与世界其他地方也发现的事物相关,例如 OEIS 中的某些列表
  • count 和 sum 非常有用,因为在操作(生成、使用、比较)批次的素数时第一个往往自然可用,并且可以轻松地在运行中轻松计算总和;这允许对现有结果进行部分验证,而无需全部实例化和更新摘要,也无需两次(相对较慢)乘法的开销
  • count 也非常有用,因为它必须是构建在摘要系统上的任何索引上层结构的一部分,相反,它可以将搜索直接引导到包含第 n 个素数的块(范围),或与第 n 个到第 (n+k) 个素数
  • 第四个组件 (checksum) 的顺序依赖性比预期的要少,因为在可能需要验证的情况下,小素数往往会按顺序“发生”(生成或使用)
  • 校验和的顺序依赖性 - 以及缺乏可组合性 - 使其在生成它的特定块之外完全无用
  • 固定大小的辅助程序结构——比如无处不在的小因子位图——最好作为原始内存进行验证,以进行启动自检,而不是在它们上运行素数摘要;这大大降低了复杂性并将速度提高了几个数量级

出于许多实际目的,可以简单地删除与顺序相关的校验和,从而为您留下一个可以轻松组合用于相邻范围的三分量摘要。

对于固定范围的验证(如在自测中),校验和组件仍然有用。任何其他类型的校验和——CRC 的道德等价物——对此同样有用,而且可能更快。如果可以找到一种顺序无关(可组合)的方式来补充前三个组件的分辨率,那将更加有用。将分辨率扩展到前三个组件之外与更大的计算工作最相关,例如为后代筛选、验证和消化数万亿个素数。

一个与顺序无关、可组合的第四个分量的候选是平方和。

尽管校验和组件存在缺陷,但总体而言,摘要还是非常有用的。查看摘要的最佳方式可能是由“特征”部分(前三个组件,可组合)和仅与特定块相关的校验和部分组成。后者也可以用任何所需分辨率的散列代替。 Kaganar 的解决方案表明了如何将这种校验和/哈希集成到一个超出单个块的系统中,尽管其固有的不可组合性。

素数来源摘要似乎已被搁置,所以这里是:

  • 多达 1,000,000,000,000 个可作为来自 primos.mat.br 等网站的文件
  • 通过primesieve.org 控制台程序(管道)以超高速批量处理高达 2^64-10*2^64
  • 通过gp/PARI 程序(管道,大约 100 万次质数/分钟),最高可达 2^64-1 - 甚至更高

【讨论】:

  • 感谢您记录您的分析——我觉得这是一篇有趣的文章。
【解决方案3】:

我在第二个答案中再次回答这个问题,因为这是一个非常不同的并且希望更好的方法:

我突然想到,您所做的基本上是在寻找校验和,而不是在素数列表中,而是在数字为素数(位设置为 1)或不是(位设置为 0)。对于任何有趣的范围,0 将比 1 多得多,因此您希望只需要对 1 进行操作。

通常,使用微不足道的任意顺序散列的问题是它们处理多重性很差并且忘记了排序。但是您不必关心这些问题中的任何一个——每个位只能设置或取消设置一次。

从这个角度来看,如果结合位索引的良好散列函数(即找到的素数),按位异或或加法应该没问题。 (如果你的素数是 64 位的,你可以使用一些函数 here。)

因此,为了最终的简单性,可以为任何一组输入范围提供相同的值,是的,坚持散列并将它与像你一样的简单操作相结合。但是更改为根据其输入显示为“随机”的传统哈希函数——链接页面上的hash64shift 可能是您正在寻找的。有意义的碰撞的可能性很小。然而,大多数散列函数都很糟糕——确保你选择了一个已知具有良好属性的函数。 (雪崩很好,等等)Thomas Wang 通常不会那么糟糕。 (鲍勃詹金的很棒,但他主要坚持32位功能。虽然他在链接页面上的混合功能非常好,但可能有点矫枉过正。)

并行化检查显然是微不足道的,与我的其他答案相比,代码大小和工作量大大减少,同步少得多,几乎不需要缓冲。

【讨论】:

  • 最自然的输入形式是素数序列,因为实际的存储可能会有很大差异。向量、列表、复合或素数的打包位图(即相同但反转)、运行字节差异(比位图更快、更紧凑),甚至使用轮子的更紧凑的形式,比如著名的 mod 30 轮子,它有效地将 30 个数字塞入一个字节。关于雪崩:杂音混合器非常好。它(看似)简单、优雅且高效。美丽的东西。
  • 是的,鲍勃·詹金斯的工作理应获得奖章。对于在该领域工作的每个人来说,这是一本必读的书,也是一种灵感。
猜你喜欢
  • 2022-07-29
  • 1970-01-01
  • 1970-01-01
  • 2012-12-27
  • 2017-08-20
  • 2020-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多