调用一个小函数两次(例如在if条件和主体中)比将结果存储在局部变量中更可取

Is calling a small function twice (e.g. in if condition and body) preferable to storing result in a local variable?

本文关键字:主体 结果 存储 局部变量 条件 函数 一个 两次 if 调用      更新时间:2023-10-16

下面的两个选项中哪一个更好(或更可取),为什么?

ReturnType *function1(const ParamType *param) {
const ValueType* value = getSomeValue(param);
if (value) {
return value->finalStuff();
}
return nullptr;
}

ReturnType *function2(const ParamType *param) {
if (getSomeValue(param)) {
return getSomeValue(param)->finalStuff();
}
return nullptr;
}

给定getSomeValue,如:

ValueType * getSomeValue(const ParamType *param) {
if (param) {
return param->some.very.boring.stuff.value;
}
return nullptr;
}

有更好的选择吗?

冒着看起来像意见(以及重述注释)的风险:使用变量声明。它更好地表达了意图:获取一个指针,如果不为空,则使用It。它肯定不会更贵(指针位于寄存器中!),如果getSomeValue非常复杂(或变得非常复杂),它可能会便宜得多。如果getSomeValue有副作用,或者其他线程可能会更改它返回的值,那么您还可以避免第二个调用与第一个调用不同的问题(读取:当第一个调用不是时为null)。

您可以通过在if:中声明value来避免重复

ReturnType *function1(const ParamType *param) {
if (const ValueType* const value = getSomeValue(param)) {
return value->finalStuff();
}
return nullptr;
}

由于问题是标记性能,它取决于getSomeValue()是否内联,以及调用getSomeValue()是否具有可观察的效果(包括任何同步,如访问互斥、原子变量等)。

然而,在您的特定情况下,clang、GCC和VC++生成相同的代码(链接到godbolt):

getSomeValue(ParamType const*):           # @getSomeValue(ParamType const*)
test    rdi, rdi
je      .LBB0_1
mov     rax, qword ptr [rdi]
ret
.LBB0_1:
xor     eax, eax
ret
function1(ParamType const*):               # @function1(ParamType const*)
test    rdi, rdi
je      .LBB1_1
mov     rax, qword ptr [rdi]
test    rax, rax
je      .LBB1_3
mov     rax, qword ptr [rax]
ret
.LBB1_1:
xor     eax, eax
ret
.LBB1_3:
xor     eax, eax
ret
function2(ParamType const*):               # @function2(ParamType const*)
test    rdi, rdi
je      .LBB2_1
mov     rax, qword ptr [rdi]
test    rax, rax
je      .LBB2_3
mov     rax, qword ptr [rax]
ret
.LBB2_1:
xor     eax, eax
ret
.LBB2_3:
xor     eax, eax
ret

(GCC生成function2作为jmp function1,但当只定义了两个函数中的一个时,生成相同的代码)

因此,这实际上取决于个人偏好/代码可读性。

better是主观的,可能涉及性能问题、可读性或代码维护。


性能

你的函数getSomeValue没有任何副作用,是纯属性的候选者:

对除了返回值之外对程序状态没有明显影响的函数的调用可能有助于优化,如公共子表达式消除。使用pure属性声明此类函数可以使GCC避免在重复调用具有相同参数值的函数时发出一些调用。

纯属性禁止函数通过检查函数返回值以外的其他方式修改可观察到的程序状态。然而,使用纯属性声明的函数可以安全地读取任何非易失性对象,并以不影响其返回值或程序的可观察状态的方式修改对象的值。

例如,

int hash (char *) __attribute__ ((pure));

告诉GCC,如果哈希可观察到的程序状态(包括数组本身的内容)在两者之间不发生变化,则使用相同字符串对函数哈希的后续调用可以替换为第一次调用的结果。[…]

纯函数的一些常见示例是strlenmemcmp

因此,由于优化,在第二个示例中调用两次getSomeValue(param)的性能不会比存储返回值差。


可读性

答案取决于函数和变量的命名方式。对于一般建议,请尽可能少地编写代码,只提供明确有用的信息。

在第一个示例中,变量value没有向程序添加任何有用的信息。我更喜欢第二种方法,因为调用getSomeValue(param)清楚地指示getter从指定参数返回属性。getter可以在代码中重复调用,这在面向对象编程中很常见。

如果在返回值中添加附加信息,那么您的第一个示例可能会更好。


维护

第一个示例没有添加比第二个更有用的信息,同时多包含1个变量和1行代码,这会导致混乱和更多的代码维护。


结论

如果您为函数getSomeValue添加纯属性,那么您的两个示例在性能方面是等效的。IMHO,你的第二个例子更短,所以更好。

当我读到第二个变体时,我本能地想知道这个函数是否真的是无状态的,并且是否真的保证在第二次调用时会产生相同的结果。这是一个精神上的坑。当函数采用更多(复杂)参数时,情况更为严重,我还需要单独检查这些参数,以确定是否可以预期这两个调用会产生相同的结果。

相比之下,第一个变体只调用一次函数,检查结果,并且在调用value->finalStuff()时,毫无疑问value不会是空指针。

因此,即使第一个变体多使用了一个变量(更复杂的语法),它也比第二个变体更容易推理。后来才是真正重要的,imho。


关于性能,第一种变体保证至少与第二种变体一样快,因为在CPU寄存器中缓存指针总是比调用函数快。如果编译器不能对第二个函数调用进行优化,那么它总是纯粹的开销。如果编译器知道函数的定义,这可能是可能的,但如果在另一个翻译单元中实现,则可能是不可能的。


最后,我支持Davis-Hering的回答:将变量声明移动到if()条件中的语法是专门为方便此用例而设计的。在适当的情况下使用此工具,这是有问题的代码的情况。