【问题标题】:speeding up "base conversion" for large integers加快大整数的“基本转换”
【发布时间】:2010-11-25 04:45:42
【问题描述】:

我正在使用基本转换算法从大整数(拆分为 32 位字)生成排列。

我为此使用了一个相对标准的算法:

/* N = count,K is permutation index (0..N!-1) A[N] contains 0..N-1 */
i = 0;
while (N > 1) {
   swap A[i] and A[i+(k%N)]
   k = k / N
   N = N - 1
   i = i + 1
}

不幸的是,每次迭代的除法和模数加起来,尤其是移动到大整数 - 但是,我似乎可以只使用乘法!

/* As before, N is count, K is index, A[N] contains 0..N-1 */
/* Split is arbitrarily 128 (bits), for my current choice of N */
/* "Adjust" is precalculated: (1 << Split)/(N!) */
a = k*Adjust; /* a can be treated as a fixed point fraction */
i = 0;
while (N > 1) {
   a = a*N;  
   index = a >> Split;         
   a = a & ((1 << Split) - 1);  /* actually, just zeroing a register */       
   swap A[i] and A[i+index]
   N = N - 1
   i = i + 1
}

这更好,但做大整数乘法仍然很慢。

问题 1:
有没有更快的方法?

例如。既然我知道 N*(N-1) 小于 2^32,我可以从一个单词中提取这些数字,并合并到“剩菜”中吗?
或者,有没有办法修改算术解码器以一次提取一个索引?

问题 2:
出于好奇 - 如果我使用乘法将数字转换为以 10 为底的数字而不进行调整,则结果将乘以 (10^digits/2^shift)。有没有一种棘手的方法来消除这个与十进制数字一起工作的因素?即使有调整因素,这似乎会更快——为什么标准库不使用这个与除法和模法?

【问题讨论】:

  • 我无法理解你的第二种算法。
  • @GregS - 如果您认为有问题,请告诉我 - 理论是它使用乘法/掩码从左侧 (msb) 删除值,而使用 mod/divide 从右侧 (lsb) 删除值。

标签: c algorithm optimization math


【解决方案1】:

看到你在谈论像 2^128/(N!) 这样的数字,在你的问题中,N 似乎会相当小(根据我的计算,N

i = 2;
while (i < N) {
    swap A[N - 1 - i] and A[N - i + k % i]
       k = k / i
       i = i + 1
}

现在更改循环以在每次迭代中执行多个排列。我猜除i的速度是一样的,只要i 将范围 2...N-1 拆分为子范围,使每个子范围中的数字的乘积小于 2^32:

2, 3, 4, ..., 12: product is 479001600
13, 14, ..., 19:  product is 253955520
20, 21, ..., 26:  product is 3315312000
27, 28, ..., 32:  product is 652458240
33, 34, 35:       product is 39270

然后,将长数 k 除以乘积,而不是除以 i。每次迭代都会产生一个余数(小于 2^32)和一个较小的数 k。当你有剩余时,你可以使用原始算法在内部循环中使用它;现在会更快,因为它不涉及长除法。
这是一些代码:

static const int rangeCount = 5;
static const int rangeLimit[rangeCount] = {13, 20, 27, 33, 36};
static uint32_t rangeProduct[rangeCount] = {
    479001600,
    253955520,
    3315312000,
    652458240,
    39270
};

for (int rangeIndex = 0; rangeIndex < rangeCount; ++rangeIndex)
{
    // The following two lines involve long division;
    // math libraries probably calculate both quotient and remainder
    // in one function call
    uint32_t rangeRemainder = k % rangeProduct[rangeIndex];
    k /= rangeProduct[rangeIndex];

    // A range starts where the previous range ended
    int rangeStart = (rangeIndex == 0) ? 2 : rangeLimit[rangeIndex - 1];

    // Iterate over range
    for (int i = rangeStart; i < rangeLimit[rangeIndex] && i < n; ++i)
    {
        // The following two lines involve a 32-bit division;
        // it produces both quotient and remainder in one Pentium instruction
        int remainder = rangeRemainder % i;
        rangeRemainder /= i;
        std::swap(permutation[n - 1 - i], permutation[n - i + remainder]);
    }
}

当然,这段代码可以扩展到128位以上。
另一种优化可能涉及从范围乘积中提取 2 的幂;通过使范围更长,这可能会稍微加快速度。不确定这是否值得(可能对于较大的 N 值,例如 N=1000)。

【讨论】:

    【解决方案2】:

    不知道算法,但是你使用的那些看起来很简单,所以我真的不明白你如何优化算法。

    您可以使用其他方法:

    • 使用 ASM(汇编器)——根据我的经验,经过很长时间试图弄清楚应该如何在 ASM 中编写某种算法后,它最终比编译器生成的版本要慢:) 可能是因为编译器还知道如何布局代码以使 CPU 缓存更有效,和/或哪些指令实际上更快以及在哪些情况下(这是在 GCC/linux 上)。
    • 使用多处理:
      • 使您的算法成为多线程的,并确保您使用与可用 cpu 内核数量相同的线程数运行(现在大多数 cpu 确实有多个内核/多线程)
      • 让您的算法能够在网络上的多台机器上运行,并设计一种将这些数字发送到网络中的机器的方法,这样您就可以使用它们的 CPU 能力。

    【讨论】:

    • -1 因为这些建议都不是好建议 - 第一个很少是针对任何性能问题的好建议,而第二个似乎对 this 问题。当然,如果你能建议如何并行化,我会很乐意撤销我的投票。
    • 1:自定义 ASM 实际上很好,但前提是您知道自己在做什么并且可移植性不是实际问题(如果它始终在特定硬件上运行)2:我假设这个算法在forlike循环中被调用了很多次,否则速度就无关紧要了。在这种情况下,循环可以分成更小的部分并并行运行。
    猜你喜欢
    • 2011-09-09
    • 1970-01-01
    • 2010-10-28
    • 1970-01-01
    • 2010-09-26
    • 2010-10-25
    • 2021-06-24
    • 2016-09-19
    • 1970-01-01
    相关资源
    最近更新 更多