在C ++中,我是否要为自己不吃的东西付费?

时间:2018-09-21 11:07:37

标签: c++ c

让我们考虑以下C和C ++的世界示例:

main.c

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

main.cpp

#include <iostream>

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

当我将它们编译成汇编时,C代码的大小只有9行(gcc -O3):

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret

但是C ++代码的大小为22行(g++ -O3):

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edx, 11
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        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     edi, OFFSET FLAT:_ZSt4cout
        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
        add     rsp, 8
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

...更大。

众所周知,在C ++中,您需要为所吃的东西付费。那么,在这种情况下,我要支付什么?

13 个答案:

答案 0 :(得分:209)

  

那么,在这种情况下,我要支付多少钱?

std::coutprintf更强大,更复杂。它支持语言环境,有状态格式标志等等。

如果您不需要这些,请使用std::printfstd::puts-它们在<cstdio>中可用。


  

众所周知,在C ++中,您需要为所吃的东西付费。

我还想说明C ++ != C ++标准库。标准库应该是通用的,并且“足够快”,但是它通常会比您所需的专门实现慢。

另一方面,C ++语言努力使编写代码成为可能,而无需支付不必要的额外隐藏成本(例如,选择加入virtual,不进行垃圾回收)。

答案 1 :(得分:173)

您没有在比较C和C ++。您正在比较printfstd::cout,它们具有不同的功能(语言环境,有状态格式等)。

尝试使用以下代码进行比较。 Godbolt会为两个文件生成相同的程序集(已通过gcc 8.2,-O3测试)。

main.c:

#include <stdio.h>

int main()
{
    int arr[6] = {1, 2, 3, 4, 5, 6};
    for (int i = 0; i < 6; ++i)
    {
        printf("%d\n", arr[i]);
    }
    return 0;
}

main.cpp:

#include <array>
#include <cstdio>

int main()
{
    std::array<int, 6> arr {1, 2, 3, 4, 5, 6};
    for (auto x : arr)
    {
        std::printf("%d\n", x);
    }
}

答案 2 :(得分:133)

您的列表确实在比较苹果和橙子,但并非出于大多数其他答案中所隐含的原因。

让我们检查一下代码的实际作用:

C:

  • 打印单个字符串"Hello world\n"

C ++:

  • 将字符串"Hello world"流到std::cout
  • std::endl机械手流到std::cout

显然,您的C ++代码正在完成两倍的工作。为了进行公平的比较,我们应该结合以下内容:

#include <iostream>

int main()
{
    std::cout<<"Hello world\n";
    return 0;
}

…突然之间,您为main编写的汇编代码看起来与C十分相似:

main:
        sub     rsp, 8
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        xor     eax, eax
        add     rsp, 8
        ret

实际上,我们可以逐行比较C和C ++代码,并且差异很小

sub     rsp, 8                      sub     rsp, 8
mov     edi, OFFSET FLAT:.LC0   |   mov     esi, OFFSET FLAT:.LC0
                                >   mov     edi, OFFSET FLAT:_ZSt4cout
call    puts                    |   call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
xor     eax, eax                    xor     eax, eax
add     rsp, 8                      add     rsp, 8
ret                                 ret

唯一的区别是在C ++中,我们用两个参数(operator <<和字符串)调用std::cout。我们可以通过使用更接近的C eeqvalent:fprintf来消除甚至细微的差异,它也具有指定流的第一个参数。

这将为_GLOBAL__sub_I_main留下汇编代码,该代码是为C ++而不是C生成的。这是在此汇编清单中唯一可见的真实开销(对于两者语言,当然)。该代码在C ++程序开始时一次性执行了一些C ++标准库函数的设置。

但是,如其他答案所述,这两个程序之间的相关区别不会在main函数的汇编输出中找到,因为所有繁重的工作都在后台进行。

答案 3 :(得分:58)

您需要付费的是调用一个繁重的库(不如打印到控制台中那么繁重)。您初始化一个ostream对象。有一些隐藏的存储。然后,调用std::endl,它不是\n的同义词。 iostream库可帮助您调整许多设置,并减轻处理器而不是程序员的负担。这就是您要支付的。

让我们检查一下代码:

.LC0:
        .string "Hello world"
main:

初始化ostream对象+ cout

    sub     rsp, 8
    mov     edx, 11
    mov     esi, OFFSET FLAT:.LC0
    mov     edi, OFFSET FLAT:_ZSt4cout
    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)

再次调用cout以打印新行并刷新

    mov     edi, OFFSET FLAT:_ZSt4cout
    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
    add     rsp, 8
    ret

静态存储初始化:

_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

此外,区分语言和库也很重要。

顺便说一句,这只是故事的一部分。您不知道正在调用的函数中写了什么。

答案 4 :(得分:53)

  

众所周知,在C ++中,您需要为所吃的东西付费。所以在这种情况下   我要支付什么?

那很简单。您需要支付serve。 “您只为吃的东西付钱”并不意味着“您总能获得最优惠的价格”。当然,std::cout更便宜。有人可能会说printf更安全,用途更广,因此有理由证明其较高的成本是合理的(成本更高,但提供的价值却更高),但却没有抓住重点。您不使用std::cout,而是使用printf,因此您需要为使用std::cout付费。您无需为使用std::cout而付费。

一个很好的例子是虚函数。虚拟函数具有一些运行时成本和空间要求-但仅当您实际使用它们时。如果您不使用虚拟功能,则无需支付任何费用。

一些评论

  1. 即使C ++代码求出更多的汇编指令,它仍然是少数指令,而且任何性能开销都可能与实际的I / O操作相形见

  2. 实际上,有时它甚至比“在C ++中为所吃的东西付费”还要好。例如,编译器可以推断出在某些情况下不需要虚拟函数调用,并将其转换为非虚拟调用。这意味着您可以免费获得免费虚拟函数。那不是很好吗?

答案 5 :(得分:47)

“ printf的程序集列表”不是用于printf,而是用于puts(是对编译器进行优化吗?); printf比pret复杂得多……别忘了!

答案 6 :(得分:44)

我在这里看到了一些有效的答案,但是我将在细节上做更多的说明。

如果您不想遍历整个文本墙,请跳到下面的摘要以获取主要问题的答案。


抽象

  

那么,在这种情况下,我要支付多少钱?

您要为抽象 付款。能够编写更简单,更人性化的代码是有代价的。在C ++(这是一种面向对象的语言)中,几乎所有东西都是对象。当您使用任何对象时,总会有三件事发生:

  1. 对象创建,基本上是对象本身及其数据的内存分配。
  2. 对象初始化(通常通过某些init()方法)。通常,内存分配是在幕后进行的,这是第一步。
  3. 对象破坏(并非总是如此)。

您没有在代码中看到它,但是每次您使用一个对象时,以上三种情况都需要以某种方式发生。如果您要手动执行所有操作,则代码显然会更长一些。

现在,可以在不增加开销的情况下高效地进行抽象:编译器和程序员都可以使用方法内联和其他技术来消除抽象的开销,但这不是您的情况。

C ++中到底发生了什么?

在这里,细分了:

  1. std::ios_base类已初始化,它是所有与I / O相关的基础类。
  2. std::cout对象已初始化。
  3. 您的字符串将被加载并传递到std::__ostream_insert,该字符串(如您所知道的那样是std::cout的方法(基本上是<<运算符),该方法会添加一个字符串到流中。
  4. cout::endl也传递给std::__ostream_insert
  5. __std_dso_handle传递给__cxa_atexit,这是一个全局函数,负责在退出程序之前进行“清理”。 __std_dso_handle本身被此函数调用以取消分配并销毁剩余的全局对象。

所以使用C ==不支付任何费用吗?

在C代码中,发生的步骤很少:

  1. 您的字符串已加载,并通过puts寄存器传递给edi
  2. puts被呼叫。

任何地方都没有对象,因此无需初始化/销毁任何东西。

但这并不意味着您没有为C中的任何东西“付款” 。您仍然需要支付抽象费用,还需要为C标准库初始化和printf函数(或者实际上是puts的动态解析)方面的费用,该函数由编译器进行了优化,因为您不需要任何格式字符串)仍在幕后进行。

如果要用纯汇编语言编写此程序,它将看起来像这样:

jmp start

msg db "Hello world\n"

start:
    mov rdi, 1
    mov rsi, offset msg
    mov rdx, 11
    mov rax, 1          ; write
    syscall
    xor rdi, rdi
    mov rax, 60         ; exit
    syscall

基本上,这只会导致调用write syscall,然后调用exit系统调用。现在,将是完成同一件事的最低要求。


总结

C更加简陋,并且只需要最低的要求,从而完全由用户控制,从而可以完全优化和自定义所需的任何内容。您告诉处理器将字符串加载到寄存器中,然后调用库函数以使用该字符串。另一方面, C ++更复杂,更抽象。这在编写复杂的代码时具有巨大的优势,并允许编写起来更容易和更人性化的代码,但这显然是有代价的。在这种情况下,与C相比,C ++的性能始终存在一个缺点,因为 C ++提供的功能远远超过完成这些基本任务所需的功能,因此会增加更多的开销

回答您的主要问题

  

我要为不吃的东西付费吗?

在这种情况下,。您并没有利用C ++所不能提供的任何优势,而仅仅是因为C ++可以帮助您的那段简单的代码中没有任何东西:它是如此的简单以至于您根本不需要C ++。 / p>


哦,还有一件事!

乍看之下,C ++的优势似乎并不明显,因为您编写了一个非常简单且很小的程序,但是看了一些更复杂的示例并发现了不同(两个程序做的完全一样):

C

#include <stdio.h>
#include <stdlib.h>

int cmp(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}

int main(void) {
    int i, n, *arr;

    printf("How many integers do you want to input? ");
    scanf("%d", &n);

    arr = malloc(sizeof(int) * n);

    for (i = 0; i < n; i++) {
        printf("Index %d: ", i);
        scanf("%d", &arr[i]);
    }

    qsort(arr, n, sizeof(int), cmp)

    puts("Here are your numbers, ordered:");

    for (i = 0; i < n; i++)
        printf("%d\n", arr[i]);

    free(arr);

    return 0;
}

C ++

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main(void) {
    int n;

    cout << "How many integers do you want to input? ";
    cin >> n;

    vector<int> vec(n);

    for (int i = 0; i < vec.size(); i++) {
        cout << "Index " << i << ": ";
        cin >> vec[i];
    }

    sort(vec.begin(), vec.end());

    cout << "Here are your numbers:" << endl;

    for (int item : vec)
        cout << item << endl;

    return 0;
}

希望您能在这里清楚地理解我的意思。另请注意,在C语言中,您必须如何使用mallocfree在较低级别上管理内存,如何在索引和大小方面需要更加小心,以及在输入和输入时如何非常具体。打印。

答案 7 :(得分:27)

开始时会有一些误解。首先,C ++程序不会生成22条指令,它更像是22,000条指令(我从帽子上拉出了这个数字,但大约在球场上)。同样,C代码也不会产生9条指令。这些只是您所看到的。

C代码的作用是,在做了很多您看不见的事情之后,它从CRT调用了一个函数(通常但不一定以共享库的形式出现),然后没有< / em>检查返回值或处理错误,然后解决。根据编译器和优化设置的不同,它甚至不是真正调用printf,而是真正调用puts,甚至更原始。
如果只用相同的方式调用相同的函数,那么您可能也已经或多或少用C ++编写了相同的程序(除了一些不可见的init函数)。或者,如果您想超级正确,则该函数以std::为前缀。

实际上,相应的C ++代码根本不是一回事。尽管整个<iostream>都是一头丑陋的肥猪,但它为小型程序(在“真正的”程序中,您并未真正注意到的东西)增加了巨大的负担而闻名。它做了很多您看不到的东西,哪个可以正常工作。包括但不限于几乎所有杂乱无章的东西的神奇格式,包括不同的数字格式和区域设置以及诸如此类,以及缓冲以及适当的错误处理。错误处理?好吧,是的,猜测是什么,输出字符串实际上可能会失败,并且与C程序不同,C ++程序会 not 静默忽略它。考虑一下std::ostream的功能,并且没有任何人知道,它实际上是轻量级的。不喜欢使用它,因为我非常讨厌流语法。但是,如果您考虑一下它的作用,那就太棒了。

但是可以肯定的是,C ++总体上还不如C高效。它不可能是一样高效,因为它不是同一件事,也不是同一件事。如果没有别的,C ++会生成异常(以及在其上生成,处理或失败的代码),并提供C不会提供的某些保证。因此,可以肯定的是,C ++程序一定需要更大一点。但是,从总体上看,这没有任何关系。相反,对于 real 程序,我很少发现C ++性能更好,因为出于某种原因,它似乎有助于进行更有利的优化。别问我为什么特别不知道。

如果您不是在乎最好的方法,而是在编写正确的C代码(例如,您实际上在检查错误,并且程序在如果有错误),则差异很小(如果存在)。

答案 8 :(得分:22)

您正在为错误付出代价。在80年代,当编译器不足以检查格式字符串时,运算符重载被视为在io期间强制执行某种类型安全性的好方法。但是,它的每个横幅功能要么从一开始就实施得很差,要么在概念上已经破产:

C ++流io api中最令人反感的部分是此格式标头库的存在。除了有状态,丑陋和易于出错之外,它还将格式化与流结合在一起。

假设您要打印出一行,该行包含8位零填充的十六进制unsigned int,后跟一个空格,后跟一个带小数点后3位的double。使用<cstdio>,您可以读取简洁的格式字符串。使用<ostream>,您必须保存旧状态,将对齐方式设置为右,设置填充字符,设置填充宽度,将基数设置为十六进制,输出整数,恢复保存的状态(否则您的整数格式将污染您的浮点格式),输出空格,将符号设置为固定,设置精度,输出双精度和换行符,然后恢复旧格式。

// <cstdio>
std::printf( "%08x %.3lf\n", ival, fval );

// <ostream> & <iomanip>
std::ios old_fmt {nullptr};
old_fmt.copyfmt (std::cout);
std::cout << std::right << std::setfill('0') << std::setw(8) << std::hex << ival;
std::cout.copyfmt (old_fmt);
std::cout << " " << std::fixed << std::setprecision(3) << fval << "\n";
std::cout.copyfmt (old_fmt);

操作员超载

<iostream>是如何不使用运算符重载的代名词:

std::cout << 2 << 3 && 0 << 5;

性能

std::coutprintf()几倍。猖atur的特征性炎和虚拟派遣确实造成了损失。

线程安全

<cstdio><iostream>都是线程安全的,因为每个函数调用都是原子的。但是,printf()每次通话可以完成更多工作。如果使用<cstdio>选项运行以下程序,则只会看到f行。如果您在多核计算机上使用<iostream>,则可能还会看到其他内容。

// g++ -Wall -Wextra -Wpedantic -pthread -std=c++17 cout.test.cpp

#define USE_STREAM 1
#define REPS 50
#define THREADS 10

#include <thread>
#include <vector>

#if USE_STREAM
    #include <iostream>
#else
    #include <cstdio>
#endif

void task()
{
    for ( int i = 0; i < REPS; ++i )
#if USE_STREAM
        std::cout << std::hex << 15 << std::dec;
#else
        std::printf ( "%x", 15);
#endif

}

int main()
{
    auto threads = std::vector<std::thread> {};
    for ( int i = 0; i < THREADS; ++i )
        threads.emplace_back(task);

    for ( auto & t : threads )
        t.join();

#if USE_STREAM
        std::cout << "\n<iostream>\n";
#else
        std::printf ( "\n<cstdio>\n" );
#endif
}

此示例的反驳是,大多数人都遵守纪律,无论如何也绝不从多个线程写入单个文件描述符。好吧,在这种情况下,您将不得不观察到<iostream>将有助于锁定每个<<和每个>>的锁。而在<cstdio>中,您将不会经常锁定,甚至可以选择不锁定。

<iostream>花费更多的锁来获得不一致的结果。

答案 9 :(得分:18)

除了所有其他答案所说的之外,
还存在std::endl'\n'相同的事实。

不幸的是,这是一种常见的误解。 std::endl并不表示“换行”,
它的意思是“打印新行然后刷新流”。 冲洗不便宜!

暂时完全忽略printfstd::cout之间的区别,以便在功能上等同于您的C示例,您的C ++示例应该看起来像这样:

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    return 0;
}

这是一个示例,说明如果包含冲洗功能,示例应该是什么样的。

C

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    fflush(stdout);
    return 0;
}

C ++

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    std::cout << std::flush;
    return 0;
}

在比较代码时,您应该始终注意,您要进行类似的比较,并且要了解代码在做什么。有时,即使是最简单的示例,也比某些人意识到的要复杂。

答案 10 :(得分:16)

虽然现有的技术答案是正确的,但我认为问题最终源于这种误解:

  

众所周知,在C ++中,您需要为所吃的东西付费。

这只是来自C ++社区的市场营销演讲。 (为公平起见,每个语言社区都有营销演讲。)这并不意味着您可以严重依赖的任何具体内容。

“按使用量付费”应该意味着C ++功能仅在您使用该功能时才有开销。但是“功能”的定义并不是无限细化。通常,您最终会激活具有多个方面的功能,即使您只需要这些方面的子集,也常常不可行或不可能以便部分实现该功能。

通常,许多(尽管可以说不是全部)语言都努力提高效率,并取得不同程度的成功。 C ++规模庞大,但它的设计没有什么特别之处或不可思议之处,可以使其在此目标中取得圆满成功。

答案 11 :(得分:12)

C ++中的Input / Output函数编写精美,并且设计简单易用。在许多方面,它们都是C ++中面向对象功能的展示。

但是实际上您确实会放弃一些性能,但是与操作系统在较低级别上处理功能所花费的时间相比,这可以忽略不计。

您总是可以退回到C风格的函数,因为它们是C ++标准的一部分,或者完全放弃了可移植性,并直接调用操作系统。

答案 12 :(得分:1)

正如您在其他答案中看到的那样,当您在通用库中链接并调用复杂的构造函数时,您需要付费。这里没有特别的问题,更多的是抱怨。我将指出一些现实世界的方面:

  1. Barne的核心设计原则是永远不要让效率成为停留在C而不是C ++的原因。就是说,需要谨慎才能获得这些效率,并且偶尔有一些效率始终有效,但在C规范中并不是“技术上”的。例如,没有真正指定位字段的布局。

  2. 尝试浏览ostream。哦,天哪,它肿了!在那儿找到飞行模拟器,我不会感到惊讶。甚至stdlib的printf()通常也要运行约50K。这些并不是懒惰的程序员:printf大小的一半与大多数人从未使用过的间接精度参数有关。几乎每个真正受限制的处理器的库都会创建自己的输出代码,而不是printf。

  3. 大小的增加通常会提供更加封闭和灵活的体验。打个比方,自动售货机将以几枚硬币的价格出售一杯类似咖啡的物品,整个交易过程不到一分钟。进入一家好的餐厅涉及到餐桌摆放,坐着,点菜,等待,拿起漂亮的杯子,拿到账单,以您选择的形式付款,添加小费以及希望旅途中有美好的一天。这是一种不同的体验,如果您和朋友一起来吃一顿复杂的饭菜,会更加方便。

  4. 人们仍然写ANSI C,尽管很少使用K&RC。我的经验是,我们总是使用C ++编译器使用一些配置调整来编译它,以限制所拖入的内容。其他语言也有很好的论据:Go removes多态开销和疯狂的预处理器;对于更智能的字段打包和内存布局,已经有一些很好的论据。恕我直言,我认为任何语言设计都应从目标列表开始,就像Zen of Python

这是一个有趣的讨论。您问为什么不能拥有神奇的小型,简单,优雅,完整和灵活的库?

没有答案。不会有答案。那就是答案。