最快的因子实现,64位结果

时间:2010-07-08 19:17:11

标签: c++ assembly x86-64 inline-assembly factorial

这不是家庭作业,只是我喜欢的东西。因此,直接计算因子不是很快; memoization可以提供帮助,但如果结果适合32位或64位,则factorial只能分别用于输入01220。所以...我们不妨使用查找表:

n   n!
0   1       
1   1       
2   2       
3   6       
4   24      
5   120     
6   720     
7   5040        
8   40320       
9   362880      
10  3628800     
11  39916800        
12  479001600       
13  6227020800  2^32=   4294967296
14  87178291200     
15  1.30767E+12     
16  2.09228E+13     
17  3.55687E+14     
18  6.40237E+15     
19  1.21645E+17     
20  2.4329E+18      
        2^64=   1.84467E+19

所以,假设我想要一个使用内联汇编的内联C ++阶乘函数,结果需要32位或64位无符号整数。如果输入为负或大到足以导致溢出,则输出应为0.如何在汇编中完成此操作以使其消耗最少量的循环?此代码将在64位Intel / AMD架构上运行。如果可行,我有兴趣改进最坏的情况,因此20!计算的时间不应超过0! - 希望有一种二元搜索方法。希望有一个聪明的伎俩可以做if (n == 0 || n == 1) { return 1; }。此外,如果输出需要是32位,那么我认为汇编指令可以包含代码和数据。我的装配知识很薄弱。如果这个问题没有多大意义,请告诉我。

能够在C ++中使用该函数会很好 - 这使它成为一个更现实的问题。例如,如果调用函数是昂贵的,那么尝试在程序集的主体中保存1-2个时钟周期将无济于事。

7 个答案:

答案 0 :(得分:10)

我巧妙地在程序集中构建了一个查找表。为了防止你的装配生锈, 例程期望参数位于ecx寄存器中。我验证它在范围内,然后将查找表的值读入eaxedx寄存器。如果该值超出范围,我只是将eaxedx寄存器与它们相加(这会强制它们为0)。不幸的是,由于它是一个汇编例程,编译器将无法内联代码。但是,我确信通过编写我精彩的汇编程序而节省的几个周期将通过内联来弥补任何收益。

factorial:
    xorl    %eax, %eax
    xorl    %edx, %edx
    cmpl    $20, %ecx
    ja  .TOOBIG
    movl    CSWTCH.1(,%ecx,8), %eax
    movl    CSWTCH.1+4(,%ecx,8), %edx
.TOOBIG:

LOOKUP_TABLE:
    .section    .rodata
    .align 32
    .type   CSWTCH.1, @object
    .size   CSWTCH.1, 168
CSWTCH.1:
    .long   1
    .long   0
    .long   1
    .long   0
    .long   2
    .long   0
    .long   6
    .long   0
    .long   24
    .long   0
    .long   120
    .long   0
    .long   720
    .long   0
    .long   5040
    .long   0
    .long   40320
    .long   0
    .long   362880
    .long   0
    .long   3628800
    .long   0
    .long   39916800
    .long   0
    .long   479001600
    .long   0
    .long   1932053504
    .long   1
    .long   1278945280
    .long   20
    .long   2004310016
    .long   304
    .long   2004189184
    .long   4871
    .long   -288522240
    .long   82814
    .long   -898433024
    .long   1490668
    .long   109641728
    .long   28322707
    .long   -2102132736
    .long   566454140

查找表很难维护,所以我已经包含了我用来构建它的脚本

static constexpr uint64_t const_factorial(uint32_t i) {
    return (i==0)? 1: (i * const_factorial(i-1));
}

uint64_t factorial(uint32_t i) {
    switch(i) {
        case 0: return const_factorial(0);
        case 1: return const_factorial(1);
        case 2: return const_factorial(2);
        case 3: return const_factorial(3);
        case 4: return const_factorial(4);
        case 5: return const_factorial(5);
        case 6: return const_factorial(6);
        case 7: return const_factorial(7);
        case 8: return const_factorial(8);
        case 9: return const_factorial(9);
        case 10: return const_factorial(10);
        case 11: return const_factorial(11);
        case 12: return const_factorial(12);
        case 13: return const_factorial(13);
        case 14: return const_factorial(14);
        case 15: return const_factorial(15);
        case 16: return const_factorial(16);
        case 17: return const_factorial(17);
        case 18: return const_factorial(18);
        case 19: return const_factorial(19);
        case 20: return const_factorial(20);
        default: return 0;
    }
}

万一你因为我的幽默尝试而错过了它。 C ++编译器能够正确优化您的代码。正如您所看到的,我不需要对查找表,二叉搜索树或哈希进行任何花哨的操作。只是一个简单的switch语句,编译器完成了其余的工作。

答案 1 :(得分:5)

我弯曲组装肌肉已经有一段时间了,所以我只是提供一些一般的建议。

由于您事先知道所有项目的确切数量和大小,只需创建一个连续的值数组(硬编码或预先计算)。验证函数的输入(< 0或> 12/20)后,您可以使用简单的偏移量寻址来检索适当的值。这将在O(1)时间内有效。

答案 2 :(得分:1)

从 2021 年开始更新。手头有 C++17。

我想没有比下面更快的方法了。不需要汇编程序。

因为适合无符号 64 位值的阶乘数量非常少 (21),编译时 constexpr 数组将主要只使用 21*8 = 168 个字节。

168 字节

这个数字很低,我们可以轻松构建编译时间 public class MyActivity extends AppcompatActivity{ MediaPlayer mp ; NumberPicker myPicker; ... @Override protected void onCreate(Bundle savedInstanceState){ ... myPicker = findByViewId(R.id.my_picker); myPicker.setOnValueChangedListener(this::changeValue); mp = MediaPlayer.create(this,R.raw.my_tick_sound); .... } private void changeValue(NumberPicker numberPicker, int i, int i1){ if(mp != null){ if(mp.isPlaying) mp.stop(); mp.start(); } //do logic.....// } 并停止所有进一步的考虑。

实际上一切都可以在编译时完成。

我们首先将计算阶乘的默认方法定义为 constexpr std::array 函数:

constexpr

这样,可以在编译时轻松计算阶乘。然后,我们用所有阶乘填充 constexpr unsigned long long factorial(unsigned long long n) noexcept { return n == 0ull ? 1 : n * factorial(n - 1ull); } 。我们还使用 std::array 并使其成为带有可变参数包的模板。

我们使用 constexpr 为索引 0,1,2,3,4,5, .... 创建阶乘

这很直接,并不复杂:

std::integer_sequence

这个函数将输入一个整数序列 0,1,2,3,4,... 并返回一个带有相应阶乘的 template <size_t... ManyIndices> constexpr auto generateArrayHelper(std::integer_sequence<size_t, ManyIndices...>) noexcept { return std::array<unsigned long long, sizeof...(ManyIndices)>{ { factorial(ManyIndices)... } }; };

我们知道最多可以存储 21 个值。因此我们创建了一个 next 函数,它将使用整数序列 1,2,3,4,...,20,21 调用上面的函数,如下所示:

std::array<unsigned long long, ...>

现在,终于,

constexpr auto generateArray()noexcept {
    return generateArrayHelper(std::make_integer_sequence<size_t, MaxIndexFor64BitValue>());
}

会给我们一个名为 Factorial 的编译时 constexpr auto Factorial = generateArray(); ,其中包含所有阶乘。如果我们需要第 i 个阶乘,那么我们可以简单地写 std::array<unsigned long long, 21>。运行时不会进行计算。

我不认为有更快的方法来计算阶乘。

请查看下面的完整程序:

Factorial[i]

使用 Microsoft Visual Studio Community 2019 版本 16.8.2 开发、编译和测试

使用 gcc 10.2 和 clang 11.0.1 额外编译和测试

语言:C++17

答案 3 :(得分:0)

谁说你的程序集版本要比C ++版本更快。事实上,谁说它甚至会在速度上匹配?我打赌100美元,你甚至无法像编译器一样快速地做到这一点。

答案 4 :(得分:0)

在流行的需求上,从表面上讲,它是传说中的二元搜索,而不是哈希表(std C ++没有我相信的那样)。

#include <map>

void main()
{
    std::map<int, BigIntThing> factMap;
    // insert all elements here, probably fancier ways to do this
    factMap.insert( 1 );
    factMap.insert( 1 );
    factMap.insert( 2 );
    // ....
    // to access, say 15!
    BigIntThing factMap[15]; // I think the index is right >_<
}

就是这样。订购std::map,因此如果您的BigIntThing具有比较运算符,那么一切都很好。应该有办法让const和/或static和/或global按照您想要的方式进行编译。

答案 5 :(得分:0)

如果您只使用0-19之间的数字,则哈希表或二叉树过度。只需创建一个unsigned int[20],然后查询索引:

const unsigned int FACTORIALS[20] = {1,1,2,6,24,120,etc..};

unsigned int factorial(unsigned int num) {
    if(num >= 0 && num <= 19) {
        return FACTORIALS[num];
    }
    else {
        throw // some sort of exception
    }
}

您也可以使用模板来构建阵列。

答案 6 :(得分:0)

gcc的答案

......可能打败了你的,来自:

uint64_t answers[] = {
    1ULL,
    1ULL,
    2ULL,
    6ULL,
    24ULL,
    ...
    2432902008176640000ULL,
};

uint64_t factorial(unsigned int i) {
    if(i >= sizeof(answers) / sizeof(*answers))
        return 0;
    else
        return answers[i];
}

......和集会......

factorial:
    cmpl    $20, %edi
    movl    $0, %eax
    ja  .L3
    movslq  %edi,%eax
    movq    answers(,%rax,8), %rax
.L3:
    rep
    ret
answers:
    .quad 1
    .quad 1
    ...

...这似乎是第一个64位汇编程序的答案......