在c++中默认参数的成本

时间:2022-11-05 11:12:04

I stumbled through an example from "Effective C++ in an Embedded Environment" by Scott Meyers where two ways of using default parameters were described: one which was described as costly and the other as a better option.

我偶然发现了Scott Meyers的“嵌入式环境中的有效c++”示例,其中描述了使用默认参数的两种方式:一种被描述为昂贵,另一种被描述为更好的选择。

I am missing the explanation of why the first option might be more costly compared to the other one.

我没有解释为什么第一种选择可能比另一种更昂贵。

void doThat(const std::string& name = "Unnamed"); // Bad

const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better

3 个解决方案

#1


56  

In the first one, a temporary std::string is initialised from the literal "Unnamed" each time the function is called without an argument.

在第一个例子中,一个临时的std::string每次在没有参数的情况下调用函数时,都会从字面上的“无名”初始化。

In the second case, the object defaultName is initialised once (per source file), and simply used on each call.

在第二种情况下,对象defaultName被初始化一次(每个源文件),并仅在每次调用中使用。

#2


18  

void doThat(const std::string& name = "Unnamed"); // Bad

This is "bad" in that a new std::string with the contents "Unnamed" is created every time doThat() is called.

这是“不好的”,因为每次调用doThat()时都会创建一个新的带有“无名”内容的std::string。

I say "bad" and not bad because the small string optimization in every C++ compiler I've used will place the "Unnamed" data within the temporary std::string created at the call site and not allocate any storage for it. So in this specific case, there is little cost to the temporary argument. The standard does not require the small string optimization, but it is explicitly designed to permit it, and every standard library currently in use implements it.

我说“坏”,也不错,因为我使用的每一个c++编译器中的小字符串优化都会将“未命名”数据放入临时std::在调用站点中创建的字符串,而不为它分配任何存储。所以在这个特定的情况下,临时论点的成本很小。该标准不需要小的字符串优化,但它被显式地设计为允许它,并且当前使用的每个标准库都实现它。

A longer string would cause an allocation; the small string optimization works on short strings only. Allocations are expensive; if you use the rule of thumb that one allocation is 1000+ times more expensive than a usual instruction (multiple microseconds!), you won't be far off.

一个更长的字符串会导致分配;小字符串优化只适用于短字符串。分配是昂贵的;如果您使用的经验法则是,一个分配比通常的指令(多微秒!

const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better

Here we create a global defaultName with the contents "Unnamed". This is created at static initialization time. There are some risks here; if doThat is called at static initialization or destruction time (before or after main runs), it could be invoked with an unconstructed defaultName or one that has already been destroyed.

在这里,我们创建一个全局defaultName,其内容为“无名”。这是在静态初始化时创建的。这里有一些风险;如果在静态初始化或销毁时(主运行之前或之后)调用doThat,则可以使用未构造的defaultName或已经被销毁的名称来调用它。

On the other hand, there is no risk that a per-call memory allocation will occur here.

另一方面,这里不存在每次调用内存分配的风险。


Now, the right solution in modern is:

现在,现代c++17的正确解决方案是:

void doThat(std::string_view name = "Unnamed"); // Best

which won't allocate even if the string is long; it won't even copy the string! On top of that, in 999/1000 cases this is a drop-in replacement to the old doThat API and it can even improve performance when you do pass data into doThat and not rely on the default argument.

即使字符串很长,它也不会分配;它甚至不会复制字符串!最重要的是,在999/1000的情况下,这是对旧的doThat API的一种替代,当您将数据传递到doThat而不依赖于默认参数时,它甚至可以提高性能。

At this point, support in the embedded may not be there, but in some cases it could be shortly. And string view is a large enough performance increase that there are a myriad of similar types already in the wild that do the same thing.

此时,嵌入式系统中的c++17支持可能并不存在,但在某些情况下,它可能很快就会存在。而string view的性能已经有了很大的提升,在野外已经有无数类似的类型在做同样的事情。

But the lesson still remains; don't do expensive operations in default arguments. And allocation can be expensive in some contexts (especially the embedded world).

但教训依然存在;不要在默认参数中执行昂贵的操作。在某些情况下(特别是嵌入式世界),分配可能很昂贵。

#3


3  

Maybe I misinterpret "costly" (for the "correct" interpretation see the other answer), but one thing to consider with default parameters is that they dont scale well in situations like that:

也许我误解了“昂贵”(对于“正确”的解释,请参阅另一个答案),但是有一件事需要考虑,默认参数在这种情况下不能很好地伸缩:

void foo(int x = 0);
void bar(int x = 0) { foo(x); }

This becomes an error prone nightmare once you add more nesting because the default value has to be repeated in several places (ie costly in the sense that one tiny change requires to change different places in the code). The best way to avoid that is like in your example:

一旦您添加了更多的嵌套,这将成为一个容易出错的噩梦,因为默认值必须在多个位置重复(即代价高昂,因为一个微小的更改需要更改代码中的不同位置)。避免这种情况的最好方法是你的例子:

const int foo_default = 0;
void foo(int x = foo_default);
void bar(int x = foo_default) { foo(x); } // no need to repeat the value here

#1


56  

In the first one, a temporary std::string is initialised from the literal "Unnamed" each time the function is called without an argument.

在第一个例子中,一个临时的std::string每次在没有参数的情况下调用函数时,都会从字面上的“无名”初始化。

In the second case, the object defaultName is initialised once (per source file), and simply used on each call.

在第二种情况下,对象defaultName被初始化一次(每个源文件),并仅在每次调用中使用。

#2


18  

void doThat(const std::string& name = "Unnamed"); // Bad

This is "bad" in that a new std::string with the contents "Unnamed" is created every time doThat() is called.

这是“不好的”,因为每次调用doThat()时都会创建一个新的带有“无名”内容的std::string。

I say "bad" and not bad because the small string optimization in every C++ compiler I've used will place the "Unnamed" data within the temporary std::string created at the call site and not allocate any storage for it. So in this specific case, there is little cost to the temporary argument. The standard does not require the small string optimization, but it is explicitly designed to permit it, and every standard library currently in use implements it.

我说“坏”,也不错,因为我使用的每一个c++编译器中的小字符串优化都会将“未命名”数据放入临时std::在调用站点中创建的字符串,而不为它分配任何存储。所以在这个特定的情况下,临时论点的成本很小。该标准不需要小的字符串优化,但它被显式地设计为允许它,并且当前使用的每个标准库都实现它。

A longer string would cause an allocation; the small string optimization works on short strings only. Allocations are expensive; if you use the rule of thumb that one allocation is 1000+ times more expensive than a usual instruction (multiple microseconds!), you won't be far off.

一个更长的字符串会导致分配;小字符串优化只适用于短字符串。分配是昂贵的;如果您使用的经验法则是,一个分配比通常的指令(多微秒!

const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better

Here we create a global defaultName with the contents "Unnamed". This is created at static initialization time. There are some risks here; if doThat is called at static initialization or destruction time (before or after main runs), it could be invoked with an unconstructed defaultName or one that has already been destroyed.

在这里,我们创建一个全局defaultName,其内容为“无名”。这是在静态初始化时创建的。这里有一些风险;如果在静态初始化或销毁时(主运行之前或之后)调用doThat,则可以使用未构造的defaultName或已经被销毁的名称来调用它。

On the other hand, there is no risk that a per-call memory allocation will occur here.

另一方面,这里不存在每次调用内存分配的风险。


Now, the right solution in modern is:

现在,现代c++17的正确解决方案是:

void doThat(std::string_view name = "Unnamed"); // Best

which won't allocate even if the string is long; it won't even copy the string! On top of that, in 999/1000 cases this is a drop-in replacement to the old doThat API and it can even improve performance when you do pass data into doThat and not rely on the default argument.

即使字符串很长,它也不会分配;它甚至不会复制字符串!最重要的是,在999/1000的情况下,这是对旧的doThat API的一种替代,当您将数据传递到doThat而不依赖于默认参数时,它甚至可以提高性能。

At this point, support in the embedded may not be there, but in some cases it could be shortly. And string view is a large enough performance increase that there are a myriad of similar types already in the wild that do the same thing.

此时,嵌入式系统中的c++17支持可能并不存在,但在某些情况下,它可能很快就会存在。而string view的性能已经有了很大的提升,在野外已经有无数类似的类型在做同样的事情。

But the lesson still remains; don't do expensive operations in default arguments. And allocation can be expensive in some contexts (especially the embedded world).

但教训依然存在;不要在默认参数中执行昂贵的操作。在某些情况下(特别是嵌入式世界),分配可能很昂贵。

#3


3  

Maybe I misinterpret "costly" (for the "correct" interpretation see the other answer), but one thing to consider with default parameters is that they dont scale well in situations like that:

也许我误解了“昂贵”(对于“正确”的解释,请参阅另一个答案),但是有一件事需要考虑,默认参数在这种情况下不能很好地伸缩:

void foo(int x = 0);
void bar(int x = 0) { foo(x); }

This becomes an error prone nightmare once you add more nesting because the default value has to be repeated in several places (ie costly in the sense that one tiny change requires to change different places in the code). The best way to avoid that is like in your example:

一旦您添加了更多的嵌套,这将成为一个容易出错的噩梦,因为默认值必须在多个位置重复(即代价高昂,因为一个微小的更改需要更改代码中的不同位置)。避免这种情况的最好方法是你的例子:

const int foo_default = 0;
void foo(int x = foo_default);
void bar(int x = foo_default) { foo(x); } // no need to repeat the value here