考虑以下代码:
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);
}
我更喜欢第一个,因为它很好并且可读性强,但是我开始怀疑如果这样做太多会失去一些性能,因为它会创建多余的副本。
我知道你们中的一些人认为第二个功能同样可读,但这只是一个例子,我想问的更多,在这种情况下什么是好的编码实践。
答案 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)
是否可以提高性能以代替这样写:
好吧,您是否衡量了性能提升?当您关心性能时,请务必进行评估。
性能可能会有所不同。如果width
和height
的类型以及参数不是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 是斜边的。
我真的希望这可以帮助您了解什么是什么。查看组装输出的其他答案(我认为知道下面发生的事情是个好主意)。