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

时间:2019-07-04 13:10:11

标签: c++

考虑以下代码:

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);
}

我更喜欢第一个,因为它很好并且可读性强,但是我开始怀疑如果这样做太多会失去一些性能,因为它会创建多余的副本。

我知道你们中的一些人认为第二个功能同样可读,但这只是一个例子,我想问的更多,在这种情况下什么是好的编码实践。

6 个答案:

答案 0 :(得分:9)

作为经验法则:编写可读的代码。

作为一种语言,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,则不必更改正文。

答案 1 :(得分: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

因此,在可以进行编译器优化的情况下,可读性优先于简短性。

答案 2 :(得分:2)

  

是否可以提高性能以代替这样写:

好吧,您是否衡量了性能提升?当您关心性能时,请务必进行评估。

性能可能会有所不同。如果widthheight的类型以及参数不是float,则局部变量会引入两次转换,必须执行。执行转换可能比不执行转换要慢,并且也可能导致不同的结果。可以通过使用auto类型推导来弥补对类型的这种依赖性。

但是,如果我们假定不涉及任何转换,则没有理由假定优化器在两种情况下均无法生成完全相同的程序,在这种情况下,性能将完全相同。您可以比较编译器生成的程序集进行验证。

  

我问的更多,在这种情况下什么是好的编码实践。

默认情况下,可读性更为重要。在这种情况下,可读性通常也是主观的。

如果衡量瓶颈,性能可能会更重要,但是这种变化不太可能实现性能。

答案 3 :(得分:1)

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

如果您想完全了解自己的情况,可以检查编译器输出(可能告诉编译器保留中间文件,在gcc -S -save-temps上),也可以在https://gcc.godbolt.org/在线检查。

答案 4 :(得分:0)

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

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

因此,我更喜欢第二种方式:假设函数的类型更改为double而不是假定的float?也就是说,写作

auto x = width / 2;

会更容易接受。

如今,许多调试器都支持逐参数评估,因此您无法通过以第一方式编写代码来帮助调试。

答案 5 :(得分:0)

我将把它分为两种情况。

性能

这两种情况都经过优化,可以编译成几乎相同的代码。万一您关闭优化功能,第一个会比今天的处理器慢一些,但并不重要。在内存方面,第一个会占用更多内存(不进行优化)而不是占用sizeof (Vector2f()),而是会占用sizeof (Vector2F()) + 2 * sizeof(double),这取决于架构sizeof (double)在英特尔处理器上是32位还是64位,例。但是在当今世界,这两个方面的要点将是相似的。

可读性 抱歉,但我不同意第二个可读性不如第一个。我认为,除非您与4个或更多运算符进行计算,否则应该找到第二个。另外,使用长名称的变量有时会使代码不可读。

double hypotenuse (double a, double b) {
    return sqrt (a * a + b * b);
}

上面的示例即使有4个运算符(不是参数),仍然可以阅读

double hypotenuse (double sideA, double sideB) {
    return sqrt (sideA * sideA + sideB * sideB);
}

第二个示例在可读性方面要差很多,因为您必须阅读,但不必一眼就能看出来,但这还不是世界末日,您仍然可以阅读很容易

您的第二个示例介于这两个示例之间,因此并非难以理解。

此功能绝对是灾难:

double hypotenuse (double sideOfTriangleA, double sideOfTriangleB) {
    return sqrt (sideOfTriangleA * sideOfTriangleA + sideOfTriangleB * sideOfTriangleB);
}

这样的简单代码使复杂,如果您了解数学,您就会知道三角形的边是 a b c c 是斜边的。

我真的希望这可以帮助您了解什么是什么。查看组装输出的其他答案(我认为知道下面发生的事情是个好主意)。