看过先前的问题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;

但是阅读程序集显示没有任何 react 。

我什至使用了__builtin_expect,但是没有用。

有什么方法可以强制编译器优化器读取for循环并利用已知输出数据的优势?

我想不使用模板元编程就这样做。

PS。 我的真正目的只是测试编译器,而不是一种计算质数的有效方法。我只想向我的 friend 们展示C++编译器功能强大。

如果需要关注is_prime的分离,我将所有内容都放在了main中,则没有发现任何区别:
#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:
...

最佳答案

这里对编译器有一个基本的误解。让我们仔细检查一下您编写的程序,并考虑您希望编译器为您做些什么。

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

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

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

  • 如果程序花费太多时间执行该怎么办?如果其中有一个错误使它运行了意外的时间怎么办?甚至无法保证program will ever halt。编译器到底应该做什么?
  • 最终,编译器的目的不是执行源代码,而是发出可能被优化的功能等效的 native 代码。毕竟,如果程序不接受任何输入(例如您的代码),则可以轻松地编译代码并运行一次以查看输出。 无论如何,都必须通过编译器或运行可执行二进制文件来执行代码,并且两种方法花费的时间相同。在两种情况下,都必须编译和执行代码。因此,这种优化没有任何实际值(value)。 但是,这与模板相反,模板旨在在编译时将其简化为常规代码。另外,对于这样的程序,解释将是更好的执行模型。您不想麻烦编译代码吗?继续使用Python解释器或其他解释器。
  • 通常,实现这种优化可能很困难或不可能。如果用于发出输出的机制有可能改变 future 输出值的副作用该怎么办?当您写入cout时,编译器并不完全知道会发生什么。标准输出流可以重定向到奇怪的东西。因此,不接受任何输入的程序不一定会使编译器更容易优化。

  • 也就是说,可以在非常有限的时间内评估的简单代码实际上是由编译器评估的。这种优化称为constant folding。无需执行即可消除对程序状态没有任何影响的代码段。例如,如果删除了cout<<i<<endl;,则编译器将仅优化其余代码。这称为dead code elimination。编译器之所以进行这些优化,是因为它们可以由编译器以系统的方式完成,并且它们在实际的代码库中非常有效。

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

    07-24 09:45
    查看更多