性能大杀器:与std::endl的较量

文摘   科技   2024-04-29 12:06   北京  

你好,我是雨乐!

相信很多人跟我一样,在学了C++基本语法之后,对于需要有换行的需求,无脑使用std::endl,这个习惯一直沿用至今,大概有十几年了吧,虽然知道这个有性能上的缺陷,但因为习惯使然或者本着存在即合理的原则,还是有意无意中使用。

std::endl

std::endl 是 C++ 标准库中的一个 manipulator(操作符),用于向输出流插入一个换行符,并刷新输出流。它的定义位于 <iostream> 头文件中。

具体来说,std::endl 的作用是将当前缓冲区中的内容输出到设备(例如终端、文件等),然后在输出流中插入一个换行符,并强制刷新缓冲区,确保输出内容立即显示在设备上。这个换行符的具体形式取决于操作系统,通常是 '\n'

使用 std::endl 的优点是它确保输出的立即可见,并且可以在输出流中插入一个换行符,使得代码更加清晰易读。

上面说过,使用std::endl来输出换行并刷新缓冲区,即:

#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

相当于:

#include <iostream>

int main() {
    std::cout << "Hello, World!" << '\n' << std::flush;
    return 0;
}

其实,源码实现远比我们上述这个复杂的多。

gcc中对std::endl的实现如下:

template<typename _CharT, typename _Traits>
     basic_ostream<_CharT, _Traits>& 
     endl(basic_ostream<_CharT, _Traits>& __os)
     { return flush(__os.put(__os.widen('\n'))); }

flush() 是刷新缓冲区,如果缓冲区中有数据的话,则显示在终端或者其他外接设备上。

除了上面的flush()操作,endl的具体实现依赖于编译:

扩展换行符(如上的widen操作),改代码:获取与流关联的当前区域设置(std::ios_base::getloc)使用此区域设置的“facet”(通过调用 std::use_facet)来执行可能的扩展检查 std::has_facet(loc) 是否为 true,以确认区域设置和流所识别的 facet如果此检查结果为 false,则抛出std::bad_cast否则,返回有效的facet将可能扩展的换行符输出到输出流(通过调用 std::basic_ostream<CharT,Traits>::put)

widen声明如下:

template<typename CharT, typename Traits>
CharT std::basic_ios< CharT, Traits >::widen    (     char     c     )     const;

在上面,提到了locale和facet两个概念,这个是关于本地化,而facet则是对本地化提供的一种支持,这块内容较多,理解起来比较抽象,可以查看相关资料。

从以上可以看出,std::endl不仅仅是刷新缓冲区这么简单,里面的操作涉及到其它很多方面,所以有时候为了我们直观上的显示,使用std::endl做了很多我们用不到的需求,所以,如果仅仅只是刷新缓冲区的话,可以考虑另一种方案。

备用

可以使用\n来替代std::endl:

void fun(std::ostream &os) {
  os << "Hello world!\n";
}

如果需要刷新缓冲区,则可以:

void fun(std::ostream &os) {
  os << "Hello world!\n" << std::endl;
}

对比

既然本文的主题是使用\n来替代std::endl,那么就得给出详实的理由,测试用例如下:

#include <iostream>
#include <chrono>
int main() {
    int num = 1000;

    auto start_endl = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < num; ++i) {
        std::cout << "Hello" << std::endl;
    }
    auto fin_endl = std::chrono::high_resolution_clock::now();
    auto duration_endl = std::chrono::duration_cast<std::chrono::microseconds>(fin_endl - start_endl);

    
    auto start_newline = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < num; ++i) {
        std::cout << "Hello \n";
    }
    auto fin_newline = std::chrono::high_resolution_clock::now();
    auto duration_newline = std::chrono::duration_cast<std::chrono::microseconds>(fin_newline - start_newline);
    std::cout << "Time with 'endl': " << duration_endl.count() << " microseconds\n";
    std::cout << "Time with '\\n': " << duration_newline.count() << " microseconds\n";

    return 0;
}

输出如下:

// ...
Time with 'endl'1977 microseconds
Time with '\n'44 microseconds

从上述输出可以看出,使用endl的耗时是使用\n的40多倍

如果从汇编的角度分析下面这块代码:

void UseEndl() {
    std::cout << "Hello world!" << std::endl;
}

void UseNewline() {
    std::cout << "Hello world!\n";
}

其汇编输出如下:

UseEndl():
        push    rbx
        mov     edx, 12
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     rax, QWORD PTR std::cout[rip]
        mov     rax, QWORD PTR [rax-24]
        mov     rbx, QWORD PTR std::cout[rax+240]
        test    rbx, rbx
        je      .L10
        cmp     BYTE PTR [rbx+56], 0
        je      .L5
        movsx   esi, BYTE PTR [rbx+67]
.L6:
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >::put(char)
        pop     rbx
        mov     rdi, rax
        jmp     std::basic_ostream<char, std::char_traits<char> >::flush()
.L5:
        mov     rdi, rbx
        call    std::ctype<char>::_M_widen_init() const
        mov     rax, QWORD PTR [rbx]
        mov     esi, 10
        mov     rax, QWORD PTR [rax+48]
        cmp     rax, OFFSET FLAT:_ZNKSt5ctypeIcE8do_widenEc
        je      .L6
        mov     rdi, rbx
        call    rax
        movsx   esi, al
        jmp     .L6
.L10:
        call    std::__throw_bad_cast()
.LC1:
        .string "Hello world!\n"
UseNewline():
        mov     edx, 13
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:std::cout
        jmp     std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
_GLOBAL__sub_I_UseEndl():
        sub     rsp, 8
        mov     edi, OFFSET FLAT:std::__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:std::__ioinit
        mov     edi, OFFSET FLAT:std::ios_base::Init::~Init() [complete object destructor]
        add     rsp, 8
        jmp     __cxa_atexit

我们不必去挨个分析汇编语句,单单从汇编行数来看,UseEndl生成了35行汇编语句,而UseNewline生成了13行汇编语句

以上~~

如果对本文有异议或者有其他技术问题,可以加微信交流:

推荐阅读  点击标题可跳转

1、性能大杀器:智能指针的资源管理

2、性能大杀器:lambda的前世今生

3、性能大杀器:inline,超乎你想象

雨乐聊编程
毕业于中国科学技术大学,现任某互联网公司高级技术专家一职。本公众号专注于技术交流,分享日常有意思的技术点、线上问题等,欢迎关注