代码之家  ›  专栏  ›  技术社区  ›  gd1234

编译器会知道跳过这个循环吗?

  •  1
  • gd1234  · 技术社区  · 1 年前

    我正在探索C++与Python等其他语言相比的速度。以下代码计数为1亿,并打印出最终数字(1亿)。我想知道编译器是否能够判断变量n只用于打印,因此它可以完全跳过循环,只打印出最终值。

    #include <iostream>
    
    int main() {
        size_t n = 0;
        while (n< 100'000'000) {
            n++;
    
        }
        
        std::cout << n << std::endl;
    
        return 0;
    }
    

    我想也许答案在于查看汇编代码,但我真的不确定这是如何工作的。

    3 回复  |  直到 1 年前
        1
  •  2
  •   Joel    1 年前

    编译器会知道跳过这个循环吗?

    取决于。

    如果您在没有启用优化的情况下进行编译,编译器会执行您告诉他的任何操作,并且不会优化循环。如果您在启用优化的情况下进行编译,编译器可以识别您正在做的事情,并跳过整个循环。

    我已经在上用gcc编译了您的代码 Compiler Explorer .


    没有优化( -O0 ):

    main:
            push    rbp
            mov     rbp, rsp
            sub     rsp, 16
            mov     QWORD PTR [rbp-8], 0
            jmp     .L2
    .L3:
            add     QWORD PTR [rbp-8], 1
    .L2:
            cmp     QWORD PTR [rbp-8], 99999999
            jbe     .L3
            mov     rax, QWORD PTR [rbp-8]
            mov     rsi, rax
            mov     edi, OFFSET FLAT:std::cout
            call    std::basic_ostream<char, std::char_traits<char> >::operator<<(unsigned long)
            mov     esi, OFFSET FLAT:std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
            mov     rdi, rax
            call    std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
            mov     eax, 0
            leave
            ret
    

    的说明 jmp .L2 高达 jbe .L3 是您编程的循环。


    通过优化( -Os ):

    main:
            push    rax
            mov     esi, 100000000
            mov     edi, OFFSET FLAT:std::cout
            call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<unsigned long>(unsigned long)
            mov     rdi, rax
            call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
            xor     eax, eax
            pop     rdx
            ret
    

    这个 mov esi, 100000000 存储的值 n 而不经过整个循环。


    我已经选择了优化级别 -Os 因为它应该生成最小的可执行文件,这意味着要查看的程序集更少(注意,更少的程序集并不一定意味着执行更快)。但是,编译器会删除已经处于最低优化级别的循环( -O1 ).

    有关gcc优化级别的概述,您可以查看 this answer .

        2
  •  2
  •   Christian Stieber    1 年前

    “在尝试中学习”。

    #include <boost/timer/timer.hpp>
    #include <iostream>
    
    int main()
    {
        size_t n = 0;
        {
            boost::timer::auto_cpu_timer t;
            while (n<100000000UL)
            {
                n++;
            }
        }
        std::cout << n << std::endl;
    }
    

    以下是未优化的情况:

    stieber@gatekeeper:~ $ g++ Test.cpp -lboost_timer && ./a.out
     0.465022s wall, 0.460000s user + 0.000000s system = 0.460000s CPU (98.9%)
    100000000
    

    这是通过优化实现的:

    stieber@gatekeeper:~ $ g++ -O2 Test.cpp -lboost_timer && ./a.out
     0.000008s wall, 0.000000s user + 0.000000s system = 0.000000s CPU (n/a%)
    100000000
    

    对于基本上什么都不做的事情来说,这是一个相当大的区别,所以唯一实际的优化来解释这一点的是去除循环。。。

    但是,让我们尝试其他方法:将一行替换为

    volatile size_t n = 0;
    

    现在,让我们来看看:

    stieber@gatekeeper:~ $ g++ Test.cpp -lboost_timer && ./a.out
     0.538811s wall, 0.520000s user + 0.000000s system = 0.520000s CPU (96.5%)
    100000000
    stieber@gatekeeper:~ $ g++ -O2 Test.cpp -lboost_timer && ./a.out
     0.558776s wall, 0.540000s user + 0.000000s system = 0.540000s CPU (96.6%)
    100000000
    

    这一次,我们没有显著的差异,因为编译器必须处理 volatile 操纵是“可见的副作用”,所以它不能优化它们。。。

        3
  •  1
  •   masoud    1 年前

    是的,它知道。事实上,在C++现代编译器中,围绕这类代码有一个聪明的优化,比如 gcc clang .

    编译器将重申它可以在编译阶段计算结果,并将结果放入进程中。

    您可以搜索以下内容: SCEV最终价值置换 树优化 。而这个 question .

    gcc有一个特定的选项:

    -ftree scev cprop

    执行最终值替换。如果在循环中修改变量,使其退出循环时的值可以为 仅使用其初始值和循环次数确定 迭代、用这样的计算代替最终值的使用, 只要它足够便宜。这减少了数据依赖性 可以允许进一步简化。默认情况下在-O1和 较高的。