【问题标题】:Most efficient way to find prime factorization of 2 for a positive integer为正整数找到 2 的素数分解的最有效方法
【发布时间】:2020-10-20 16:06:38
【问题描述】:

我正在用 C 编写代码,并希望找出最有效的方法来确定 2 除以多少次;即 5 = 0, 8 = 3。我的问题是,使用此代码,我利用按位运算来加快运行时间,总体代码为 O(log N),我可以做些什么计算或分析来优化此代码?

int Prime_Factor_Two(int n) {
    int k = 0;
    while(~(n&1) + 2){
        n = n >> 1;
        k +=1;
    }
    return k;
}

【问题讨论】:

  • 仅对正数?
  • n进入一个数的次数是log₂(n),也就是O(1)。
  • 对于int,只有 30 个数字是 2 的正幂,其他 42 亿个数字不会匹配。您可以在 O(1) 时间内使用 switch 轻松完成此操作。
  • while(~(n&1) + 2)(三算一测)可以简化为while((n&1) == 0)(一算一测)。
  • 如果你可以使用内联汇编,一些 CPU 有内置指令,例如BSF。并且您的编译器可能有内在函数也可以在没有汇编的情况下调用它们,例如__builtin_ffs 用于 GCC 或 _BitScanForward 在 MSVC 中

标签: c optimization


【解决方案1】:

好的,你说的最有效的方法是什么? (几乎)一条汇编指令怎么样?

来自 GCC 文档(也可在 Clang 中获得):

内置函数:int __builtin_ctz (unsigned int x)

返回 x 中尾随 0 位的数量,从最低有效位位置开始。如果x 为0,则结果未定义。

unsigned Prime_Factor_Two(unsigned x) {
    return x ? __builtin_ctz(x) : 0;
}

没有函数调用,没有循环,只有一个分支。如果您知道该数字是正数,您甚至可以删除它并使用__builtin_ctz(x)

__builtin_ctz() 内置:

  • 在 x86 上应编译为单个汇编指令:TZCNT(如果支持)或BSF
  • 在 ARM 上应该编译成两条指令:RBIT + CLZ
  • 在 PowerPC 上应编译为 31 - CNTLZ(x & -x)(假设 32 位 unsigned)。
  • 在其他平台上,可能只有少量指令。

为了还支持负整数,您可以利用数字的二进制补码保留最低有效零这一事实,只需将类型从 unsigned 更改为 int

unsigned Prime_Factor_Two(int x) {
    return x ? __builtin_ctz(x) : 0;
}

【讨论】:

    【解决方案2】:

    假设只有个数字并且您的系统使用2's Complement notation,您可以首先使用看似奇怪的x = x & -x operation 隔离最低有效设置位;然后,您可以使用log2(x) function 将其转换为设置位的位置。

    这是一个测试程序:

    #include <stdio.h>
    #include <math.h>
    
    int main()
    {
        int num, ans;
        do {
            printf("Enter a number: ");
            if (scanf("%d", &num) != 1 || num == 0) break;
            ans = (int)(log2(num & -num) + 0.5);
            printf("Answer is: %d\n", ans);
        } while (num > 0);
        return 0;
    }
    

    或者,为了避免使用浮点数和数学库,您可以使用位移循环(这也适用于负值和零值):

    int main()
    {
        int num, ans;
        do {
            printf("Enter a number: ");
            if (scanf("%d", &num) != 1) break;
            num &= -num;
            for (ans = 0; num > 1; ans++) num >>= 1;
            printf("Answer is: %d\n", ans);
        } while (num > 0);
        return 0;
    }
    

    编辑:当然,以上两种方法都是人为的,没有必要的;一个带有移位的单个位掩码的简单循环就可以解决问题 - 除了零值,无论如何,它可以被 2 整除(没有余数)无限次:

    #include <stdio.h>
    
    int main()
    {
        int num, ans, bit;
        do {
            printf("Enter a number: ");
            if (scanf("%d", &num) != 1 || num == 0) break;
            for (ans = 0, bit = 1; !(num & bit); ans++) bit <<= 1;
            printf("Answer is: %d\n", ans);
        } while (1);
        return 0;
    }
    

    【讨论】:

    • 利用你的第二个代码(位移循环),你知道这是否比我发布的代码更有效,还是它依赖于编译器?是的,所有输入都是正整数,编译器使用 2 的补码,这就是为什么我在 while 循环中有 ~(n&1) + 2,这会为奇数生成 0(不再除以 2),为偶数生成 1 (继续前进)。感谢您迄今为止的帮助!
    • @ChemeComp 查看编辑!我认为第三种解决方案比您的解决方案更简单,但我不确定它会快多少(如果有的话)。
    【解决方案3】:

    一个有趣的方法是对最低有效 1 位进行二进制搜索。您甚至可以将其编码为显式无分支,尽管下面的示例并不完全这样做。但是,这种方法确实需要您知道参数类型中值的位数。

    例子:

    /*
     * Returns the number of factors of 2 in the prime factorization of the argument, or
     * returns -1 if the argument is 0.
     */
    int factor_of_two_count(uint64_t in) {
        int result = -1;
        uint64_t bottom;
        
        bottom = (in & 0xffffffffu);
        in = bottom ? bottom : (in >> 32);
        result += !bottom * 32;
    
        bottom = (in & 0xffffu);
        in = bottom ? bottom : (in >> 16);
        result += !bottom * 16;
    
        bottom = (in & 0xffu);
        in = bottom ? bottom : (in >> 8);
        result += !bottom * 8;
    
        bottom = (in & 0xfu);
        in = bottom ? bottom : (in >> 4);
        result += !bottom * 4;
    
        bottom = (in & 0x3u);
        in = bottom ? bottom : (in >> 2);
        result += !bottom * 2;
    
        bottom = (in & 0x1u);
        result += !bottom;
    
        return result;
    }
    

    但是,您的逐位循环可能会优于随机数据,因为这大致类似于六次通过这样的循环,并且只有不到 2% 的随机 64 位输入需要这么多。只有当分支错误预测问题严重影响按位循环,或者如果输入的分布偏向具有许多因子 2 的那些,这才有可能成为赢家。

    【讨论】:

      【解决方案4】:

      我可以做任何计算或分析来优化此代码吗?

      Count the consecutive zero bits (trailing) on the right with modulus division and lookup

      unsigned int v;  // find the number of trailing zeros in v
      int r;           // put the result in r
      static const int Mod37BitPosition[] = // map a bit value mod 37 to its position
      {
        32, 0, 1, 26, 2, 23, 27, 0, 3, 16, 24, 30, 28, 11, 0, 13, 4,
        7, 17, 0, 25, 22, 31, 15, 29, 10, 12, 6, 0, 21, 14, 9, 5,
        20, 8, 19, 18
      };
      r = Mod37BitPosition[(-v & v) % 37];
      

      作者解释:

      上面的代码找到了右边尾随零的个数,所以二进制 0100 将产生 2。它利用了前 32 位位置值与 37 互质这一事实,因此执行模除法37 给出了一个从 0 到 36 的唯一编号。然后可以使用小型查找表将这些数字映射到零的数量。它仅使用 4 次操作,但是索引到表中并执行模除可能使其不适用于某些情况。

      【讨论】:

        猜你喜欢
        • 2014-08-01
        • 1970-01-01
        • 2011-01-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-14
        • 2014-01-31
        相关资源
        最近更新 更多