性能与可读性:函数中的本地副本

5

请考虑以下代码:

Vector2f Box::getCenter() const
{
    const float x = width / 2;
    const float y = height / 2;

    return Vector2f(x, y);
}

这样写是否会提高性能:
Vector2f Box::getCenter() const
{
    return Vector2f(width / 2, height / 2);
}

我更喜欢第一个选项,因为它看起来更清晰易读,但我开始怀疑如果我经常这样做是否会损失一些性能,因为它会创建额外不必要的副本。

我知道有些人认为第二个函数同样易读,但这只是一个例子,我更普遍地问什么是好的编程实践。


1
现代C++编译器生成的代码在这两种情况下几乎肯定是相同的。你可以自己回答这个问题:所有编译器都提供了产生机器语言代码而不是二进制可执行文件的选项。在这两种情况下使用该选项,看看是否有任何区别。 - Sam Varshavchik
1
还要注意,“可读性”非常主观。对于有经验的程序员来说,这两种变体可能同样易读(反正我是这样认为的)。 - Some programmer dude
3
我发现第一个变量令人困惑,它们看起来很重要,但如果不详细查看周围的代码,很难猜测它们是否有意义或者具体作用是什么。 - molbdnilo
3
实际上,第二种方式对我来说更易读。我可以立即看出我们正在将宽度和高度减半。对于第一种方式,我必须将x和y存储到我的脑寄存器中,然后加载它们来解释返回结果,这会产生开销... - L. F.
1
对于给定的示例,是的,但对于更复杂的示例,它是一个很好的帮助。特别是有好的名称。halfWidthhalfHeight可能是更好的名称,除非有与意图相关的更具体的名称。 - Baldrickk
显示剩余2条评论
5个回答

10
作为经验法则:编写易读的代码。
C ++作为一种语言,旨在实现效率。 在理想的情况下,易读的代码就是高效的代码,C++ 接近这一目标。很少强制要求您为了性能而编写难以阅读的代码。即使是在这种情况下,也不应该从一开始就这样做。始终首先为易读性编写代码。只有在测量之后你才能知道什么更有效率,什么不太有效率。
这是一个用于检查编译器输出的不错工具:https://godbolt.org/。当您开启优化时,我期望任何合理的编译器都会生成非常相似的汇编代码。
这是另一个让您轻松进行基准测试的工具:http://quick-bench.com/。在知道它有区别之前,请不要开始优化事物。
编译器非常擅长优化,并且大多数时候它们比您更清楚情况。当然,您不应该做“愚蠢”的事情,例如明显地您不应该return Vector2f(width * sin(pi/2) * exp( ln( 0.5)) , height / 2);,即使超级聪明的编译器能够意识到它与原始代码是相同的。
最后但并非最不重要的是,不要忘记,您主要是为人类编写阅读和理解的代码。
PS:易读性是非常主观的。有人可能会认为第二个例子更易读,而我甚至会更进一步地编写:
Vector2f Box::getCenter() const
{
    return {width * 0.5 , height * 0.5};
}

这样做的额外好处是,如果您决定更改返回类型(或将值类型更改为int),则不一定需要更改主体。


非常好的答案。很高兴知道我的编译器会优化掉这样的东西。我将继续毫不羞耻地以第一种方式编写它 :D - Oscar
我不会点踩,但我不同意这个观点,因为我在我的回答中阐述了原因。 - Bathsheba
@Oscar 嗯,如果它被理解为“随便写,不要在意”,那么这并不是一个好的答案;)。原意是:为可读性而写,并研究编译器在性能方面所做的工作。 - 463035818_is_not_a_number
@Bathsheba,我看了你的答案,我认为实际上并没有分歧。我试图保持答案的通用性(主要是指出工具)。我添加了一个PS,更具体地涉及到了OP的代码。 - 463035818_is_not_a_number
@formerlyknownas_463035818,不是这个意思。当然我会考虑性能,但在这种情况下优先考虑可读性是很好的,因为优化后的代码是相同的。 - Oscar
我更喜欢第一个,因为如果我的函数名比如说不是像 widthheight 这样的短变量名,那么返回语句会很长,我就必须横向滚动才能看到全部。 - Oscar

6

这是一段用C++编写的代码:

Vector2f GetCenterLong(float width, float height)
{
    const float x = width / 2;
    const float y = height / 2;

    return Vector2f(x, y);
}

Vector2f GetCenterShort(float width, float height)
{
    return Vector2f(width / 2, height / 2);
}

以下是使用x64 msvc v19.21-Ox优化生成的汇编代码。您可以注意到两个函数之间没有任何区别。
__$ReturnUdt$ = 8
width$ = 16
height$ = 24
Vector2f GetCenterLong(float,float) PROC                ; GetCenterLong
        mulss   xmm1, DWORD PTR __real@3f000000
        mov     rax, rcx
        mulss   xmm2, DWORD PTR __real@3f000000
        movss   DWORD PTR [rcx], xmm1
        movss   DWORD PTR [rcx+4], xmm2
        ret     0
Vector2f GetCenterLong(float,float) ENDP                ; GetCenterLong

__$ReturnUdt$ = 8
width$ = 16
height$ = 24
Vector2f GetCenterShort(float,float) PROC             ; GetCenterShort
        mulss   xmm1, DWORD PTR __real@3f000000
        mov     rax, rcx
        mulss   xmm2, DWORD PTR __real@3f000000
        movss   DWORD PTR [rcx], xmm1
        movss   DWORD PTR [rcx+4], xmm2
        ret     0
Vector2f GetCenterShort(float,float) ENDP             ; GetCenterShort

因此,在编译器可优化的情况下,优先选择可读性而非简洁性。

1

除非存在奇怪(可能是意外的)类型转换,否则优化编译器将会产生相同的代码。

两种方法之间的主要区别在于,在第一种方式中,您可能会强制进行float类型转换。

因此,我更喜欢第二种方法:假设函数的类型改变为double而不是预期的float?话虽如此

auto x = width / 2;

现在许多调试器都支持逐个参数评估,因此通过第一种方式编写代码并没有帮助调试。

更好的做法是采用能够更容易被接受的方式。


如果类型改变会有什么影响呢?当传递给构造函数时,它会被转换成任何一种类型。 - Oscar
@Oscar:想象一下,如果width被重构为double,作为函数参数的类型。如果你忘记改变float,那么你就会有一个不必要的截断。 - Bathsheba
是的,显然可以这样做,但这种情况发生的可能性很小,因为我的编译器会警告我。而且我也可以使用“auto”代替。这只是一个示例。 - Oscar

1
有这样写的性能提升吗: 当你关心性能时,一定要进行测量。
性能可能会有所不同。如果width和height的类型以及参数与float不同,则本地变量会引入两个必须执行的转换。执行转换可能比不执行转换慢,并且还可能导致不同的结果。使用auto类型推断可以解决这种类型依赖性问题。
但是,如果我们假设没有涉及任何转换,则没有理由认为优化器不能在两种情况下产生完全相同的程序,在这种情况下,性能将完全相同。您可以比较编译器生成的汇编代码来验证。
更一般地说,好的编程实践是什么? 默认情况下,可读性更重要。通常,包括这种情况在内,可读性是主观的。
如果您测量了瓶颈,则性能可能更重要,但是这种更改不太可能实现性能提升。

0

我假设Vector2f构造函数需要两个float。然后,至少使用优化编译器,您将看不到任何区别。

如果您想确切地了解您的情况,您可以检查编译器输出(可能要求编译器保留中间文件,在gcc上使用-S -save-temps),或者您可以在https://gcc.godbolt.org/上在线检查。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接