C ++常量折叠素数循环

时间:2018-02-17 08:28:16

标签: c++ g++ compiler-optimization constantfolding

看了之前的问题12,我想知道我是否可以强制编译器为下面打印素数的代码执行常量折叠。

#include <iostream>

using namespace std;

inline bool is_prime(int n)
{
    if(n<2)
        return false;
    for(int i=2;i*i<=n;i++)
        if(n%i==0)
            return false;
    return true;
}

int main()
{
    for(int i=0;i<20;i++)
        if(is_prime(i))
            cout<<i<<endl;
    return 0;
}

我通过以下方式构建它:

g++ -O3 -S main.cpp -o main.asm

结果是一些:

2,3,5,7,11,13,17,19

我想强制编译器查看类似于

的代码
for(int x:{2,3,5,7,11,13,17,19})
    cout<<x<<endl;

cout<<  2 <<endl;
cout<<  3 <<endl;
cout<<  5 <<endl;
cout<<  7 <<endl;
cout<< 11 <<endl;
cout<< 13 <<endl;
cout<< 17 <<endl;
cout<< 19 <<endl;

但阅读大会表明没有发生。

我甚至使用过__builtin_expect但它没有用。

有没有办法强制编译器优化器读取for循环并使用输出数据已知的优势?

我想在不使用模板元编程的情况下这样做。

PS。我的真正目的只是测试编译器而不是计算素数的有效方法。我只想向朋友炫耀一下C ++编译器的强大功能。

如果is_prime的分离是关注的问题,我把所有内容放在主要内部并且没有观察到任何差异:

#include <iostream>

using namespace std;

int main()
{
    for(int n=2;n<20;n++)
    {
        bool is_prime=true;
        for(int i=2;i*i<=n;i++)
            if(n%i==0)
            {
                is_prime=false;
                break;
            }
        if(is_prime)
            cout<<n<<endl;
    }
    return 0;
}

还有一个例子仍然没有编译器的借口:

#include <iostream>
#include <vector>

using namespace std;

int prime_after6000()
{
    int n=6000;
    do
    {
        bool is_prime=true;
        for(int i=2;i*i<=n;i++)
            if(n%i==0)
            {
                is_prime=false;
                break;
            }
        if(is_prime)
            return n;
        n++;
    }while(true);
}

int main()
{
    cout<<prime_after6000()<<endl;
    return 0;
}

组件:

...
main:
.LFB1907:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $6000, %esi   ;;;;;;;;;;;;;;;;;;;; bad
.L18:
    testb   $1, %sil
    je  .L15
    movl    $2, %ecx
    jmp .L16
    .p2align 4,,10
    .p2align 3
.L17:
    movl    %esi, %eax
    cltd
    idivl   %ecx
    testl   %edx, %edx
    je  .L15
.L16:
    addl    $1, %ecx
    movl    %ecx, %eax
    imull   %ecx, %eax
    cmpl    %esi, %eax
    jle .L17
    movl    $_ZSt4cout, %edi
    call    _ZNSolsEi
    movq    %rax, %rdi
    call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
    xorl    %eax, %eax
    addq    $8, %rsp
    .cfi_remember_state
    .cfi_def_cfa_offset 8
    ret
    .p2align 4,,10
    .p2align 3
.L15:
    .cfi_restore_state
    addl    $1, %esi
    jmp .L18
    .cfi_endproc
.LFE1907:
    .size   main, .-main
    .p2align 4,,15
    .type   _GLOBAL__sub_I__Z15prime_after6000v, @function
_GLOBAL__sub_I__Z15prime_after6000v:
...

1 个答案:

答案 0 :(得分:2)

这里存在对编译器的根本误解。让我们仔细研究你编写的程序,并考虑一下编译器为你做的期望。

该程序的主要特点是它不接受任何输入,但它通过写入cout来发出输出。请注意,is_prime函数不是compiler intrinsic;编译器将其视为另一个函数。这很重要,我稍后再回过头来看。

现在,编译器将如何按照您描述的方式转换程序?它怎么能做那样的事情?也就是说,编译器如何将这两个嵌套循环转换为将整数写入cout的简单语句序列?它可能做到的唯一方法是通过执行程序来找出需要写入cout的所有值。

这没有任何意义,是吗?让我们看一下这里的大图,并考虑具有相同特征的所有程序(或语句序列);那些不接受任何输入但发出输出的人。问题将变成:为什么编译器不执行源代码而只发出写入输出值的代码?由于以下原因:

  • 如果程序花费太多时间执行该怎么办?如果它中有一个错误使它运行了一段意外的时间怎么办?甚至不能保证program will ever halt。编译器究竟应该做什么?
  • 最终,编译器的目的不是执行源代码,而是发出可能优化的功能相当的本机代码。毕竟,如果程序没有接受任何输入(比如你的代码),你可以轻松编译代码并运行一次以查看输出。 无论如何,代码必须由编译器或运行可执行二进制文件执行,并且无论哪种方式都需要相同的时间。在这两种情况下,都必须编译和执行代码。因此,这样的优化不会增加真正的价值。但是,这与模板相反,模板在编译时由编译器将打算简化为常规代码。此外,解释将是这类计划的更好的执行模式。你不想打扰编译代码?继续使用Python解释器或任何解释器。
  • 通常,实施这样的优化可能是困难的或不可能的。如果用于发出输出的机制具有可以改变未来输出值的副作用,该怎么办?编译器并不完全知道写入cout时会发生什么。标准输出流可以重定向到奇怪的东西。因此,不需要任何输入的程序对于编译器来说不一定更容易优化。

也就是说,编译器确实可以在非常有限的时间内评估简单的代码片段。此优化称为constant folding。可以在不执行它们的情况下消除对程序状态没有任何影响的代码片段。例如,如果您删除了cout<<i<<endl;,编译器将只优化其余代码。这称为dead code elimination。编译器进行这些优化是因为它们可以由编译器以系统的方式完成,因为它们在实际代码库中非常有效。

但是如果is_prime函数是编译器内在函数会发生什么?在这种情况下,编译器可能会有一个常用素数的内置表和一个非常快速的素性测试实现。然后,您可以期望编译器在主函数中多次展开loop,甚至可以完全展开,仅包含输出语句,基本上执行您正在寻找的转换。