快速实现大整数计数器(用C / C ++)

时间:2014-03-13 06:38:30

标签: c++ c performance algorithm

我的目标如下,

生成连续值,以便之前从未生成每个新值,直到生成所有可能的值。此时,计数器再次启动相同的序列。这里的要点是,所有可能的值都是在没有重复的情况下生成 (直到句点耗尽)。如果序列是简单的0,1,2,3 ......,或其他顺序无关紧要。

例如,如果范围可以仅由unsigned表示,那么

void increment (unsigned &n) {++n;}

就够了。但是,整数范围大于64位。例如,在一个地方,我需要生成256位序列。一个简单的实现类似于以下内容,只是为了说明我想要做的事情,

typedef std::array<uint64_t, 4> ctr_type;
static constexpr uint64_t max = ~((uint64_t) 0);
void increment (ctr_type &ctr)
{
    if (ctr[0] < max) {++ctr[0]; return;}
    if (ctr[1] < max) {++ctr[1]; return;}
    if (ctr[2] < max) {++ctr[2]; return;}
    if (ctr[3] < max) {++ctr[3]; return;}
    ctr[0] = ctr[1] = ctr[2] = ctr[3] = 0;
}

因此,如果ctr以全零开头,则第一个ctr[0]逐个增加,直至达到max,然后ctr[1],依此类推。如果设置了所有256位,则我们将其重置为全零,然后重新开始。

问题在于,这种实施方式非常缓慢。我目前的改进版本等同于以下内容,

void increment (ctr_type &ctr)
{
    std::size_t k = (!(~ctr[0])) + (!(~ctr[1])) + (!(~ctr[2])) + (!(~ctr[3]))
    if (k < 4)
        ++ctr[k];
    else
        memset(ctr.data(), 0, 32);

}

如果仅使用上述increment函数操作计数器,并始终从零开始,则ctr[k] == 0如果ctr[k - 1] == 0。因此值k将是小于最大值的第一个元素的索引。

我预计第一个更快,因为分支误预测每2 ^ 64次迭代只发生一次。第二种,虽然误预测只发生在每2 ^ 256次迭代中,但它不会有所作为。除了分支之外,它还需要四个按位否定,四个布尔否定和三个加法。这可能比第一次花费更多。

但是,clanggcc或英特尔icpc都会生成第二个更快的二进制文件。

我的主要问题是,是否有人知道是否有更快的方法来实施这样的计数器?如果计数器通过增加第一个整数开始,或者它实现为整数数组,则无关紧要,只要该算法生成所有2 ^ 256个256位的组合。

什么使事情变得更复杂,我还需要非均匀增量。例如,每次计数器增加K其中K > 1,但几乎总是保持不变。我目前的实施与上述类似。

为了提供更多的上下文,我使用计数器的一个地方正在使用它们作为AES-NI aesenc指令的输入。如此不同的128位整数(加载到__m128i),在经过10(或12或14,取决于密钥大小)指令轮之后,生成不同的128-bits整数。如果我一次生成一个__m128i整数,则increment的成本很小。但是,由于aesenc有相当长的延迟,我按块生成整数。例如,我可能有4个块ctr_type block[4],初始化等效于以下内容,

block[0]; // initialized to zero
block[1] = block[0]; increment(block[1]);
block[2] = block[1]; increment(block[2]);
block[3] = block[2]; increment(block[3]);

每当我需要新输出时,我incrementblock[i]乘以4,并立即生成4 __m128i输出。通过交错指令,总体而言我能够增加吞吐量,并且当使用2个64位整数作为计数器和8个块时,将每字节输出(cpB)的周期从6减少到0.9。但是,如果使用4个32位整数作为计数器,则以每秒字节数测量的吞吐量减少到一半。我知道在x86-64上,64位整数在某些情况下可能比32位更快。但我没想到这种简单的增量操作会产生如此大的差异。我已经仔细地对应用程序进行了基准测试,而increment确实是一个减慢程序的速度。由于加载到__m128i并将__m128i输出存储为可用的32位或64位整数是通过对齐指针完成的,因此32位和64位版本之间的唯一区别是计数器递增。我希望AES-NI在将整数加载到__m128i后预期会占据主导地位。但是当使用4或8个区块时,情况显然不是这样。

总而言之,我的主要问题是,如果有人知道改进上述计数器实现的方法。

5 个答案:

答案 0 :(得分:4)

这不仅缓慢,而且不可能。宇宙的总能量不足以进行2 ^ 256位的变化。这需要灰色计数器。

优化前的下一步是修复原始实现

void increment (ctr_type &ctr)
{
    if (++ctr[0] != 0) return;
    if (++ctr[1] != 0) return;
    if (++ctr[2] != 0) return;
    ++ctr[3];
}

如果不允许每个ctr[i]溢出为零,则句点将仅为4 *(2 ^ 32),如0-919,29,39,49,...99199,299,...1999,2999,3999,..., 9999

作为对评论的回复 - 第一次溢出需要2 ^ 64次迭代。慷慨,一秒钟内最多可进行2 ^ 32次迭代,这意味着程序应运行2 ^ 32秒才能完成第一次执行。那是大约136年。

修改

如果具有2 ^ 66个状态的原始实现确实是想要的,那么我建议将界面和功能更改为:

  (*counter) += 1;
  while (*counter == 0)
  {
     counter++;  // Move to next word
     if (counter > tail_of_array) {
        counter = head_of_array;
        memset(counter,0, 16);
        break;
     }
  }

关键是,溢出仍然非常罕见。几乎总是只有一个词要增加。

答案 1 :(得分:2)

如果你正在使用GCC

unsigned __int128 H = 0, L = 0;
L++;
if (L == 0) H++;

在__int128不可用的系统上

unsigned long long c[4] = { 0 };
c[0]++;
if (c[0] == 0)
{
    c[1]++;
    if (c[1] == 0)
    {
        c[2]++;
        if (c[2] == 0)
        {
            c[3]++;
        }
    }
}

使用内联汇编,使用进位标志更容易实现。不幸的是,大多数高级语言都没有办法访问它。

无论如何,这是浪费时间,因为宇宙中的粒子总数只有大约10 80 ,你甚至无法计算生命中的64位计数器

答案 2 :(得分:0)

您的两个计数器版本都没有正确递增。您实际上只计算UINT256_MAX 4次,然后再次从0开始计数,而不是计算到UINT64_MAX。这一事实很明显,您无需清除任何已达到最大值的索引,直到所有索引都达到最大值。如果您根据计数器到达所有位0的频率来测量性能,那么这就是原因。因此,您的算法不会生成256位的所有组合,这是一个明确的要求。

答案 3 :(得分:0)

您提到“生成连续值,这样以前从未生成过每个新值”

要生成一组此类值,请看线性同余生成器https://en.wikipedia.org/wiki/Linear_congruential_generator

  • 序列x =(x * 1 +1)%(power_of_2),您已经考虑过了,这只是连续的数字。

  • 序列x =(x * 13 + 137)%(2的幂),这会生成具有可预测周期的唯一数(power_of_2-1),并且唯一数看起来更“随机”,是伪的-随机。您需要求助于任意精度算术以使其起作用,还需要求助于常数乘法的所有技巧。这将为您提供一个不错的入门方法。

您还抱怨您的简单代码“慢”

在频率为4.2 Ghz,每个周期运行4个指令并使用AVX512矢量化的情况下,在具有程序的多线程版本的64核计算机上,除增量外什么都没有做,则每增量仅得到64x8x4 * 2 ^ 32 = 8796093022208增量第二,即25天内达到2 ^ 64的增量。该帖子很旧,您现在可能已经达到841632698362998292480,并且在这样的机器上运行这样的程序,并且您将在2年的时间内光荣地达到1683265396725996584960。

您还需要“直到生成所有可能的值”

您只能生成有限数量的值,具体取决于您愿意为为计算机供电所需的能量付出多少。如其他回应所述,具有128或256位数字,即使是世界上最富有的人,您也永远不会在以下情况中的第一种发生之前回避:

  • 没钱
  • 人类的尽头(没人会得到您软件的结果)
  • 燃烧来自宇宙最后粒子的能量

答案 4 :(得分:0)

使用三个模仿许多处理器上的三种加法指令的宏,可以很容易地以便携式的方式完成多字加法。

ADDcc添加两个单词,并设置进位(如果它们未签名溢出)
ADDC加两个词加进位(以前加法)
ADDCcc添加两个单词加进位,如果进位未签名,则设置进位

包含两个单词的多单词加法使用最低有效单词的ADDcc和最高有效单词的ADCC。具有两个以上单词的多单词加法形成序列ADDccADDCcc,...,ADDC。 MIPS体系结构是一种没有条件代码且因此没有进位标志的处理器体系结构。下面显示的宏实现基本上遵循MIPS处理器上用于多词添加的技术。

下面的ISO-C99代码显示了基于16位“字”的32位计数器和64位计数器的操作。我选择了数组作为基础数据结构,但是例如也可以使用struct。如果每个操作数仅包含几个单词,则struct的使用将大大加快,因为消除了数组索引的开销。人们可能希望对每个“单词”使用最宽泛的整数类型以获得最佳性能。在该问题的示例中,可能是一个包含四个uint64_t组件的256位计数器。

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>

#define ADDCcc(a,b,cy,t0,t1) \
  (t0=(b)+cy, t1=(a), cy=t0<cy, t0=t0+t1, t1=t0<t1, cy=cy+t1, t0=t0)

#define ADDcc(a,b,cy,t0,t1) \
  (t0=(b), t1=(a), t0=t0+t1, cy=t0<t1, t0=t0)

#define ADDC(a,b,cy,t0,t1) \
  (t0=(b)+cy, t1=(a), t0+t1)

typedef uint16_t T;

/* increment a multi-word counter comprising n words */
void inc_array (T *counter, const T *increment, int n)
{
    T cy, t0, t1;
    counter [0] = ADDcc (counter [0], increment [0], cy, t0, t1);
    for (int i = 1; i < (n - 1); i++) {
        counter [i] = ADDCcc (counter [i], increment [i], cy, t0, t1);
    }
    counter [n-1] = ADDC (counter [n-1], increment [n-1], cy, t0, t1);
}

#define INCREMENT (10)
#define UINT32_ARRAY_LEN (2)
#define UINT64_ARRAY_LEN (4)

int main (void)
{
    uint32_t count32 = 0, incr32 = INCREMENT;
    T count_arr2 [UINT32_ARRAY_LEN] = {0};
    T incr_arr2  [UINT32_ARRAY_LEN] = {INCREMENT};
    do {
        count32 = count32 + incr32;
        inc_array (count_arr2, incr_arr2, UINT32_ARRAY_LEN);
    } while (count32 < (0U - INCREMENT - 1));
    printf ("count32 = %08x  arr_count = %08x\n", 
            count32, (((uint32_t)count_arr2 [1] << 16) +
                      ((uint32_t)count_arr2 [0] <<  0)));

    uint64_t count64 = 0, incr64 = INCREMENT;
    T count_arr4 [UINT64_ARRAY_LEN] = {0};
    T incr_arr4  [UINT64_ARRAY_LEN] = {INCREMENT};
    do {
        count64 = count64 + incr64;
        inc_array (count_arr4, incr_arr4, UINT64_ARRAY_LEN);
    } while (count64 < 0xa987654321ULL);
    printf ("count64 = %016llx  arr_count = %016llx\n", 
            count64, (((uint64_t)count_arr4 [3] << 48) + 
                      ((uint64_t)count_arr4 [2] << 32) +
                      ((uint64_t)count_arr4 [1] << 16) +
                      ((uint64_t)count_arr4 [0] <<  0)));
    return EXIT_SUCCESS;
}

经过全面优化,该32位示例在大约一秒钟的时间内执行,而64位示例在现代PC上运行大约一分钟。程序的输出应如下所示:

count32 = fffffffa  arr_count = fffffffa
count64 = 000000a987654326  arr_count = 000000a987654326

基于内联汇编或专有扩展的宽整数类型的非便携式代码的执行速度大约是此处提供的便携式解决方案的两到三倍。