优化我! (C,表现) - 跟随喋喋不休的问题

时间:2010-08-17 06:21:38

标签: c performance optimization math bit-manipulation

感谢Bit twiddling: which bit is set?的一些非常有用的stackOverflow用户,我构建了我的函数(在问题的最后发布)。

任何建议 - 即使是小建议 - 都将不胜感激。希望它能使我的代码更好,但至少它应该教会我一些东西。 :)

概述

该函数将被称为至少10 13 次,并且可能经常被称为10 15 。也就是说,此代码很可能会运行,因此任何性能提示都会有所帮助。

此功能占计划时间的72-77%,基于分析和不同配置中的大约12次运行(优化此处不相关的某些参数)。

目前该功能平均运行50个时钟。我不确定这可以改进多少,但我很高兴看到它在30岁时运行。

重点观察

如果在计算的某个时刻你可以告诉你将返回的值很小(准确值可协商 - 比如说,低于一百万)你可以提前中止。我只对大价值感兴趣。

这就是我希望节省大部分时间的方式,而不是通过进一步的微观优化(尽管这些当然也是受欢迎的!)。

表现资讯

  • smallprimes是一个位数组(64位);平均大约8位将被设置,但它可以是0或多达12。
  • q通常是非零的。 (请注意,如果q和smallprimes为零,则函数会提前退出。)
  • r和s通常为0.如果q为零,r和s也将为0;如果r为零,则s也是。
  • 正如最后的评论所说,nu到底通常是1,所以我有一个有效的特例。
  • 特殊情况下的计算可能会出现溢出风险,但通过适当的建模,我已经证明,对于我的输入,这不会发生 - 所以不要担心这种情况。
  • 此处未定义的功能(ugcd,minuu,star等)已经过优化;无需花很长时间才能运行。 pr是一个小数组(全部在L1中)。此外,此处调用的所有函数均为pure functions
  • 但如果你真的在乎... ugcd是gcd,minuu是最小值,vals是尾随二进制0的数量,__ builtin_ffs是最左边的二进制1的位置,star是(n-1) )>> vals(n-1),pr是从2到313的素数数组。
  • 目前正在Phenom II 920 x4上进行计算,但仍然对i7或Woodcrest的优化感兴趣(如果我在其他节点上获得计算时间)。
  • 我很乐意回答您对该功能或其成员的任何问题。

它实际上做了什么

添加以响应请求。你不需要阅读这部分内容。

输入是奇数n,其中1 <1。 n&lt; 4282250400097.其他输入提供了这个特定意义上的数字因子分解:

如果数字可被3整除,则设置smallprimes&amp; 1,如果数字可被5整除,则设置smallprimes&amp; 2,如果数字可被7整除,则设置smallprimes&amp; 4,如果数字可被设置为smallprimes&amp; 8可以被11等整除,直到代表313的最高有效位。可以被素数的平方整除的数字与仅被该数字整除的数字表示不同。 (事实上​​,可以丢弃多个正方形;在另一个函数的预处理阶段,素数的多个平方&lt; = lim具有小的primes而q设置为0因此它们将被丢弃,其中lim的最佳值通过实验确定。)

q,r和s代表数字的较大因子。任何剩余因子(可能大于数字的平方根,或者如果s非零甚至可能更小)可以通过将因子除以n来找到。

一旦以这种方式回收所有因子,碱基数,1 <= b <1。 n,n是strong pseudoprime,使用代码最佳解释的数学公式计算。

到目前为止的改进

  • 提前退出测试。这显然可以节省工作,所以我做了改变。
  • 相应的函数已经内联,因此__attribute__ ((inline))不执行任何操作。奇怪的是,标记主要功能bases和一些__attribute ((hot))的助手会使性能下降近2%,我无法弄清楚为什么(但它可以通过20多次测试重现)。所以我没有做出改变。同样地,__attribute__ ((const))充其量也无济于事。我对此感到非常惊讶。

代码

ulong bases(ulong smallprimes, ulong n, ulong q, ulong r, ulong s)
{
    if (!smallprimes & !q)
        return 0;

    ulong f = __builtin_popcountll(smallprimes) + (q > 1) + (r > 1) + (s > 1);
    ulong nu = 0xFFFF;  // "Infinity" for the purpose of minimum
    ulong nn = star(n);
    ulong prod = 1;

    while (smallprimes) {
        ulong bit = smallprimes & (-smallprimes);
        ulong p = pr[__builtin_ffsll(bit)];
        nu = minuu(nu, vals(p - 1));
        prod *= ugcd(nn, star(p));
        n /= p;
        while (n % p == 0)
            n /= p;
        smallprimes ^= bit;
    }
    if (q) {
        nu = minuu(nu, vals(q - 1));
        prod *= ugcd(nn, star(q));
        n /= q;
        while (n % q == 0)
            n /= q;
    } else {
        goto BASES_END;
    }
    if (r) {
        nu = minuu(nu, vals(r - 1));
        prod *= ugcd(nn, star(r));
        n /= r;
        while (n % r == 0)
            n /= r;
    } else {
        goto BASES_END;
    }
    if (s) {
        nu = minuu(nu, vals(s - 1));
        prod *= ugcd(nn, star(s));
        n /= s;
        while (n % s == 0)
            n /= s;
    }

    BASES_END:
    if (n > 1) {
        nu = minuu(nu, vals(n - 1));
        prod *= ugcd(nn, star(n));
        f++;
    }

    // This happens ~88% of the time in my tests, so special-case it.
    if (nu == 1)
        return prod << 1;

    ulong tmp = f * nu;
    long fac = 1 << tmp;
    fac = (fac - 1) / ((1 << f) - 1) + 1;
    return fac * prod;
}

16 个答案:

答案 0 :(得分:14)

你似乎浪费了很多时间去做因素分歧。用除数的倒数替换除法要快得多(除法:〜15-80()个周期,取决于除数,乘法:~4个周期),如果当然可以预先计算倒数。

虽然这似乎不可能用 q r s - 由于这些变量的范围,这很容易与 p 有关,它总是来自小的静态 pr [] 数组。预先计算这些素数的倒数并将它们存储在另一个数组中。然后,不是除以 p ,而是乘以从第二个数组得到的倒数。 (或者制作一个结构数组。)

现在,通过这种方法获得精确的除法结果需要一些技巧来补偿舍入误差。您可以在this document,第138页找到此技术的血腥细节。

编辑:

在咨询 Hacker's Delight (一本优秀的书籍,BTW)之后,通过利用代码中的所有分歧都是准确的(即剩余部分),您似乎可以更快地实现它。零)。

似乎对于每个除数 d 是奇数且基数 B = 2 word_size ,存在唯一的乘法逆满足条件的d⃰d⃰ < Bd·d⃰ ≡ 1 (mod B)。对于每个 x ,它是 d 的精确倍数,这意味着x/d ≡ x·d⃰ (mod B)。这意味着您可以简单地用乘法替换除法,不添加更正,检查,舍入问题等等。 (这些定理的证明可以在书中找到。)注意这个乘法逆不需要等于前一个方法定义的倒数!

如何检查给定的 x d 的确切倍数 - 即x mod d = 0?简单! x mod d = 0 iff x·d⃰ mod B ≤ ⌊(B-1)/d⌋。请注意,此上限可以预先计算。

所以,在代码中:

unsigned x, d;
unsigned inv_d = mulinv(d);          //precompute this!
unsigned limit = (unsigned)-1 / d;   //precompute this!

unsigned q = x*inv_d;
if(q <= limit)
{
   //x % d == 0
   //q == x/d
} else {
   //x % d != 0
   //q is garbage
}

假设pr[]数组成为struct prime的数组:

struct prime {
   ulong p;
   ulong inv_p;  //equal to mulinv(p)
   ulong limit;  //equal to (ulong)-1 / p
}

代码中的while(smallprimes)循环变为:

while (smallprimes) {
    ulong bit = smallprimes & (-smallprimes);
    int bit_ix = __builtin_ffsll(bit);
    ulong p = pr[bit_ix].p;
    ulong inv_p = pr[bit_ix].inv_p;
    ulong limit = pr[bit_ix].limit;
    nu = minuu(nu, vals(p - 1));
    prod *= ugcd(nn, star(p));
    n *= inv_p;
    for(;;) {
        ulong q = n * inv_p;
        if (q > limit)
            break;
        n = q;
    }
    smallprimes ^= bit;
}

对于mulinv()函数:

ulong mulinv(ulong d) //d needs to be odd
{
   ulong x = d;
   for(;;)
   {
      ulong tmp = d * x;
      if(tmp == 1)
         return x;
      x *= 2 - tmp;
   }
}

请注意,您可以将ulong替换为任何其他无符号类型 - 只需始终使用相同的类型。

本书中提供了证明,为什么如何。衷心推荐阅读: - )。

答案 1 :(得分:5)

如果编译器支持GCC函数属性,则可以使用以下属性标记纯函数:

ulong star(ulong n) __attribute__ ((const));

该属性向编译器指示函数的结果仅取决于其参数。优化器可以使用此信息。

您是否有开放式vals()而非使用__builtin_ctz()的原因?

答案 2 :(得分:5)

目前还不清楚,你在寻找什么。数值理论问题经常通过推导出解决方案必须满足的数学特性来实现巨大的加速。

如果您确实正在搜索最大化MR测试的非证人数量的整数(即您提到的oeis.org/classic/A141768)那么可能会使用非证人的数量不能大于phi(n)/ 4并且具有这么多非证人的整数要么是形式的两个素数的乘积

  

(K + 1)*(2K + 1)

或者他们是Carmichael数字,有3个素因子。 我认为在上面的某些限制中,序列中的所有整数都具有这种形式,并且可以通过证明所有其他整数的证人的上限来验证这一点。 例如。具有4个或更多因子的整数最多只有phi(n)/ 8个非证人。对于其他整数的基数,可以从您的公式中得出类似的结果。

对于微优化:每当你知道一个整数可以被一个商整除时,就有可能用一个乘以模2 ^ 64的商的倒数来代替除法。测试n%q == 0可以用测试替换

  

n * inverse_q&lt; max_q,

其中inverse_q = q ^( - 1)mod 2 ^ 64且max_q = 2 ^ 64 / q。 显然,inverse_q和max_q需要预先计算,才能有效,但由于你使用筛子,我认为这不应该是一个障碍。

答案 3 :(得分:4)

小优化但是:

ulong f;
ulong nn;
ulong nu = 0xFFFF;  // "Infinity" for the purpose of minimum
ulong prod = 1;

if (!smallprimes & !q)
    return 0;

// no need to do this operations before because of the previous return
f = __builtin_popcountll(smallprimes) + (q > 1) + (r > 1) + (s > 1);
nn = star(n);

BTW:您应该编辑帖子以添加star()和您使用定义的其他功能

答案 4 :(得分:4)

尝试替换此模式(对于r和q也是如此):

n /= p; 
while (n % p == 0) 
  n /= p; 

有了这个:

ulong m;
  ...
m = n / p; 
do { 
  n = m; 
  m = n / p; 
} while ( m * p == n); 

在我的有限测试中,我通过消除模数得到了一个小的加速(10%)。

此外,如果p,q或r是常数,编译器将通过乘法替换除法。如果p,q或r几乎没有选择,或者某些选项更频繁,那么你可以通过专门为这些值设置函数来获得一些东西。

答案 5 :(得分:3)

您是否尝试过使用配置文件引导优化?

使用-fprofile-generate选项编译并链接程序,然后在代表性数据集上运行程序(比如,一天的计算值)。

然后重新编译并将其与-fprofile-use选项链接起来。

答案 6 :(得分:2)

1)我会让编译器吐出它生成的程序集,并尝试推断它的作用是否能做到最好......如果发现问题,请更改代码以使程序集看起来更好。通过这种方式,您还可以确保您希望它内联的函数(如star和vals)真正内联。 (您可能需要添加编译指示,甚至将它们转换为宏)

2)你在多核机器上试试这个很棒,但是这个循环是单线程的。我猜测有一个伞形函数可以将负载分成几个线程,以便使用更多的核心?

3)如果实际功能试图计算的内容不清楚,很难建议加速。通常情况下,最令人印象深刻的加速并不是通过比特琐事来实现的,而是随着算法的变化而实现的。所以一些评论可能会有所帮助; ^)

4)如果您真的想要加速10 *或更高,请查看CUDA或openCL,它允许您在图形硬件上运行C程序。它充满了这些功能!

5)你正在做大量的模数并且彼此分开。在C中,这是2个单独的命令(第一个'/'然后'%')。但是在汇编中这是一个命令:'DIV'或'IDIV',它一次性返回余数和商:

B.4.75 IDIV: Signed Integer Divide

IDIV r/m8                     ; F6 /7                [8086]
IDIV r/m16                    ; o16 F7 /7            [8086]
IDIV r/m32                    ; o32 F7 /7            [386]

IDIV performs signed integer division. The explicit operand provided is the divisor; the dividend and destination operands are implicit, in the following way:

For IDIV r/m8, AX is divided by the given operand; the quotient is stored in AL and the remainder in AH.

For IDIV r/m16, DX:AX is divided by the given operand; the quotient is stored in AX and the remainder in DX.

For IDIV r/m32, EDX:EAX is divided by the given operand; the quotient is stored in EAX and the remainder in EDX.

所以它需要一些内联汇编,但我猜测会有一个显着的加速,因为你的代码中有一些地方可以从中受益。

答案 7 :(得分:2)

  1. 确保您的功能内联。如果它们不在线,则开销可能会增加,尤其是在第一个while循环中。最好的方法是检查装配。

  2. 您是否尝试过预先计算star( pr[__builtin_ffsll(bit)] )vals( pr[__builtin_ffsll(bit)] - 1)?这将为一个数组查找交换一些简单的工作,但如果表足够小,它可能是值得的。

  3. 在你真正需要之前不要计算f(接近结束时,早退之后)。您可以使用类似

  4. 的代码替换BASES_END周围的代码
    BASES_END:
    ulong addToF = 0;
    if (n > 1) {
        nu = minuu(nu, vals(n - 1));
        prod *= ugcd(nn, star(n));
        addToF = 1;
    }
    // ... early out if nu == 1...
    // ... compute f ...
    f += addToF;
    

    希望有所帮助。

答案 8 :(得分:2)

首先是一些挑剔;-)你应该更加小心你正在使用的类型。在某些地方你似乎认为ulong是64位宽,在那里使用uint64_t。对于所有其他类型,请仔细重新考虑您对它们的期望并使用适当的类型。

我能看到的优化是整数除法。你的代码做了很多,这可能是你正在做的最昂贵的事情。小整数划分(uint32_t)可能比大整数划分更有效。特别是对于uint32_t,有一个汇编指令可以一次性进行除法和模运算,称为divl

如果您使用适当的类型,您的编译器可能会为您完成所有操作。但有人已经说过,你最好检查一下汇编程序(选项-S到gcc)。否则很容易在这里和那里包含一些小的汇编程序片段。我在我的一些代码中找到了类似的东西:

register uint32_t a asm("eax") = 0;
register uint32_t ret asm("edx") = 0;
asm("divl %4"
    : "=a" (a), "=d" (ret)
    : "0" (a), "1" (ret), "rm" (divisor));

正如您所看到的,它使用特殊寄存器eaxedx以及类似的东西......

答案 9 :(得分:1)

您是否尝试过第一个while循环的表查找版本?您可以将smallprimes除以4个16位值,查找其贡献并合并它们。但也许你需要副作用。

答案 10 :(得分:1)

您是否尝试传入素数数组而不是在smallprimesqrs中将它们分开?由于我不知道外部代码的作用,我可能错了,但是你也有可能将一些素数转换为smallprimes位图,在这个函数中,你转换位图有效地回到一系列素数。此外,您似乎对smallprimesqrs的元素执行相同的处理。它应该为每次通话节省少量处理。

另外,你似乎知道传入的素数除以n。除了划分n的每个素数之外,你有足够的知识吗?如果您可以通过将该信息传递给此函数来消除模运算,则可以节省大量时间。换句话说,如果npow(p_0,e_0)*pow(p_1,e_1)*...*pow(p_k,e_k)*n_leftover,如果您对这些e_in_leftover了解得更多,那么传递它们就意味着很多事情你不会必须做这个功能。


可能有一种方法可以用较少的模数运算来发现n_leftovern的未实现部分),但它只是一种预感,所以你可能需要稍微试验一下。我的想法是使用gcd重复从n中删除已知因子,直到你摆脱所有已知的素因子。让我给出一些几乎是c代码:

factors=p_0*p_1*...*p_k*q*r*s;
n_leftover=n/factors;
do {
    factors=gcd(n_leftover, factors);
    n_leftover = n_leftover/factors;
} while (factors != 1);

我完全不确定这会比你的代码更好,更不用说你可以在其他答案中找到的组合mod / div建议,但我认为值得一试。我觉得这将是一场胜利,特别是对于那些拥有大量小素因子的数字。

答案 11 :(得分:1)

您正在传递n的完全因子分解,因此您要将连续的整数分解,然后在此处使用该分解的结果。在我看来,在找到因素时,你可能会从中做一些事情中受益。

顺便说一下,我有一些非常快速的代码,可以在不进行任何划分的情况下找到你正在使用的因素。它有点像筛子,但很快会产生连续数非常的因子。如果您认为它可能会有所帮助,可以找到并发布。

编辑必须在此处重新创建代码:

#include 
#define SIZE (1024*1024) //must be 2^n
#define MASK (SIZE-1)
typedef struct {
    int p;
    int next;
} p_type;

p_type primes[SIZE];
int sieve[SIZE];

void init_sieve()
{
    int i,n;
    int count = 1;
    primes[1].p = 3;
    sieve[1] = 1;
    for (n=5;SIZE>n;n+=2)
    {
        int flag = 0;
        for (i=1;count>=i;i++)
        {
            if ((n%primes[i].p) == 0)
            {
                flag = 1;
                break;
            }
        }
        if (flag==0)
        {
            count++;
            primes[count].p = n;
            sieve[n>>1] = count;
        }
    }
}

int main()
{
    int ptr,n;
    init_sieve();
    printf("init_done\n");

    // factor odd numbers starting with 3
    for (n=1;1000000000>n;n++)
    {
        ptr = sieve[n&MASK];
        if (ptr == 0) //prime
        {
//            printf("%d is prime",n*2+1);
        }
        else //composite
        {
//            printf ("%d has divisors:",n*2+1);
            while(ptr!=0)
            {
//                printf ("%d ",primes[ptr].p);
                sieve[n&MASK]=primes[ptr].next;
                //move the prime to the next number it divides
                primes[ptr].next = sieve[(n+primes[ptr].p)&MASK];
                sieve[(n+primes[ptr].p)&MASK] = ptr;
                ptr = sieve[n&MASK];
            }
        }
//        printf("\n");
    }
    return 0;
}

init函数创建一个因子库并初始化筛子。我的笔记本电脑大约需要13秒钟。然后,所有高达10亿的数字都会在另外25秒内被考虑或确定为素数。小于SIZE的数字从未报告为素数,因为它们在因子基数中有1个因子,但可以改变。

这个想法是为筛子中的每个条目保留一个链表。通过简单地将他们的因素从链表中拉出来来计算数字。当它们被拔出时,它们被插入列表中,以便下一个可以被该素数整除的数字。这也是非常友好的缓存。筛分尺寸必须大于因子基中的最大素数。这样,这个筛子可以在大约7个小时内运行到2 ** 40,这似乎是你的目标(除了n需要64位)。

您的算法可以合并到此中,以便在识别时使用因子,而不是将位和大素数打包成变量以传递给您的函数。或者您的函数可以更改为获取链接列表(您可以创建一个虚拟链接以传入因子库之外的素数)。

希望它有所帮助。

顺便说一句,这是我第一次公开发布这个算法。

答案 12 :(得分:1)

只是一个想法,但如果您还没有,可能使用您的编译器优化选项会有所帮助。另一个想法是,如果钱不是问题,你可以使用英特尔C / C ++编译器,假设你使用英特尔处理器。我还假设其他处理器制造商(AMD等)会有类似的编译器

答案 13 :(得分:1)

如果您要立即退出(!smallprimes&!q),为什么不在调用该函数之前进行该测试,并保存函数调用开销?

此外,您似乎有效地拥有3个不同的函数,除了smallprimes循环外,它们都是线性的。    bases1(s,n,q)bases2(s,n,q,r)bases3(s,n,q,r,s)

实际创建那些没有分支和gotos的3个独立函数可能是一个胜利,并调用适当的函数:

if (!(smallprimes|q)) { r = 0;}
else if (s) { r = bases3(s,n,q,r,s);}
else if (r) { r = bases2(s,n,q,r); }
else        { r = bases1(s,n,q);

如果先前的处理已经给调用代码一些“知识”执行哪个函数并且你不必测试它,那么这将是最有效的。

答案 14 :(得分:1)

如果您使用的分区的编号在编译时是未知的,但在运行时经常使用(多次除以相同的数字),那么我建议使用libdivide库,它基本上在运行时实现编译器为编译时常量做的优化(使用移位掩码等)。这可以提供巨大的好处。同样避免将x%y == 0用于上述建议的z = x / y,z * y == x和ergosys也应该有一个可衡量的改进。

答案 15 :(得分:0)

您帖子中的代码是优化版本吗?如果是的话,仍有太多的除法操作会大大占用CPU周期。

这段代码不必要地过了一点

if (!smallprimes & !q)
    return 0;

更改为逻辑和&amp;&amp;

if (!smallprimes && !q)
    return 0;

会使它更快地短路而不会使q

以下代码

ulong bit = smallprimes & (-smallprimes);
ulong p = pr[__builtin_ffsll(bit)];

用于查找smallprimes的最后一位。你为什么不用更简单的方法

ulong p = pr[__builtin_ctz(smallprimes)];

性能下降的另一个罪魁祸首可能是程序分支过多。您可以考虑更改为其他一些较少分支或无分支的等价物