【问题标题】:GCC/g++ cout << vs. printf()GCC/g++ cout << vs. printf()
【发布时间】:2011-03-06 16:12:40
【问题描述】:
  • 为什么 printf("hello world") 在汇编代码中使用的 CPU 指令(不考虑使用的标准库)比 cout &lt;&lt; "hello world" 更多?

对于 C++,我们有:

movl    $.LC0, %esi
movl    $_ZSt4cout, %edi
call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc

对于 C:

movl    $.LC0, %eax
movq    %rax, %rdi
movl    $0, %eax
call    printf
  • C++ 代码中的第 2 行和 C 代码中的第 2,3 行是什么?

我使用的是 gcc 版本 4.5.2

【问题讨论】:

  • 你为什么不编译一个对你自己的代码的调用(关闭内联),你可以在调用者和被调用者中看到源代码/汇编程序的关系。此外,每当您发布汇编程序时,您需要指定架构。指定 gcc 的版本也是一个好主意。
  • 如何关闭它?

标签: c++ c gcc assembly g++


【解决方案1】:

对于 Linux x86_64 上的 64 位 gcc -O3 (4.5.0),其内容为:cout

movl    $11, %edx         ; String length in EDX
movl    $.LC0, %esi       ; String pointer in ESI
movl    $_ZSt4cout, %edi  ; load virtual table entry of "cout" for "ostream"
call    _ZSt16__ostream_insertIcSt11char_traits...basic_ostreamIT_T0_ES6_PKS3_l

对于 printf("Hello World")

movl    $.LC0, %edi       ; String pointer to EDI
xorl    %eax, %eax        ; clear EAX (maybe flag for printf=>no stack arguments)
call    printf

这意味着,您的顺序完全取决于任何特定的 编译器实现,它的版本和可能的编译器选项。 您的编辑 状态,您使用 gcc 4.5.2(相当新)。 似乎 4.5.2 引入了额外的 64 位寄存器摆弄 无论出于何种原因,这个序列。它将 64 位 RAX 保存到 RDI 在将其归零之前 - 这绝对没有意义(至少对我而言)。

更有趣:3 参数调用序列(g++ -O1 -S source.cpp):

 void c_proc()
{
 printf("%s %s %s", "Hello", "World", "!") ;
}

 void cpp_proc()
{
 std::cout << "Hello " << "World " << "!";
}

导致(c_proc):

movl    $.LC0, %ecx
movl    $.LC1, %edx
movl    $.LC2, %esi
movl    $.LC3, %edi
movl    $0, %eax
call    printf

.LCx 是字符串,不涉及堆栈指针

对于 cpp_proc

movl    $6, %edx
movl    $.LC4, %esi
movl    $_ZSt4cout, %edi
call    _ZSt16__ostream_insertIcSt11char_traits...basic_ostreamIT_T0_ES6_PKS3_l
movl    $6, %edx
movl    $.LC5, %esi
movl    $_ZSt4cout, %edi
call    _ZSt16__ostream_insertIcSt11char_traits...basic_ostreamIT_T0_ES6_PKS3_l
movl    $1, %edx
movl    $.LC0, %esi
movl    $_ZSt4cout, %edi
call    _ZSt16__ostream_insertIcSt11char_traits...basic_ostreamIT_T0_ES6_PKS3_l

你现在明白这是怎么回事了。

问候

rbo

【讨论】:

  • 它在将 64 位 RAX 清零之前将其保存到 RDI - 这绝对没有意义(至少对我而言)。 实际上它很有意义,因为 %rdi 是用作 printf 函数的输入参数(有关详细信息,请参阅 x64 ABI)。实际上通过 eax/rax 没有什么意义,可能意味着编译没有任何优化。
  • @Tomek:我明白你的意思。但是我的意思是:如果将字符串指针保存到 EAX,然后将整个 RAX 处理到 RDI,与将指针直接复制到 EDI 相比,这有什么意义(如我的 4.5.0 示例)。
  • 顺便说一句,注意如果你使用普通优化,它会调用std::__ostream_insert,这需要一个字符串长度,但如果你使用-Os,它会调用std::operator&lt;&lt;,这不需要长度(我猜它是内联operator&lt;&lt; 对于非操作系统情况)。还有一点点:它没有加载虚拟表,它只是加载std::cout 作为流函数的第一个参数。
【解决方案2】:

调用者代码大部分时间与性能无关。

我猜 C++ 代码的第 2 行将 std::cout 的地址存储为 operator

我在 C 部分可能是错误的,但在我看来它是不完整的。 rax 的 32bit 上半部分在这个 sn-p 中没有初始化,它可能更早被初始化。(不,我错了)。

据我了解(我可能是错的),64 位寄存器的问题在于大多数情况下它们不能被立即数初始化,因此您必须使用 32 位操作才能获得所需的结果。所以编译器使用 32 位寄存器来初始化 64 位 rdi 寄存器。

似乎 printf 将 al 的值(eax 的 LSB)作为输入,告诉 printf() 有多少 xmm 128 寄存器用作输入。能够将输入字符串传递到 xmm 寄存器或其他一些有趣的事情看起来像是一种优化。

【讨论】:

    【解决方案3】:

    int printf( const char*, ...) 是一个可变参数函数,可以接受一个或多个参数;而ostream&amp; operator&lt;&lt; (ostream&amp;, signed char*) 正好需要两个。我相信这可以解释调用它们所需的指令的差异。

    C++ 反汇编中的第 2 行是它通过 ostream& 的地方(在本例中为 cout)。所以函数知道它输出到哪个流对象。

    由于两者最终都进行了函数调用,因此比较在很大程度上无关紧要;在函数调用中执行的代码将更加重要。运算符在小事上出汗。

    【讨论】:

    • 如果我们针对大小而不是速度进行优化,那么在某些情况下,差异可能是有意义的。
    • @junjanes:我假设在这种情况下没有进行任何优化,但我不确定评论与这个答案有什么关系。我会说差异完全是因为所涉及的两个函数都有非常不同的接口。只有苹果和橙子。
    【解决方案4】:

    movl 是长移动,32 位移动

    movq 是移动四,64 位移动

    printf 有一个返回值,可以是写入的字符数,也可以是失败时的 -1,并且该值存储在 %eax 中,这就是多余的行所担心的。

    【讨论】:

    • 我知道指令的作用,我担心为什么(为什么)代码看起来像它。 “movl $0, %eax”的意思是“将常量 0 移入 EAX”,但是为什么?
    • 可能是因为printf是可变参数函数,最后需要一个delimiter参数。
    • 我怀疑额外的代码与返回值有什么关系,因为它是在函数调用之前执行的!该函数负责在内部设置其返回值。此外 ostream::operator
    • 只是将寄存器清零,然后是参数值的数量
    • 看我的回答。 eax 与 printf() 中可能的优化有关,至于参数值的数量,我建议它不存在,因为您无法在自己的可变参数函数上检索它
    猜你喜欢
    • 2020-08-12
    • 1970-01-01
    • 2011-06-26
    • 2011-08-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-15
    • 1970-01-01
    相关资源
    最近更新 更多