问题描述
我有一个函数:f(param)
,它根据输入 (param
) 进行一次计算。这个函数应该在一个紧密的循环中被调用大约 100 万次(最大值):
for (std::uint_fast64_t i = 0; i < 1'000'000; ++i) {
f (param);
}
第一个问题:
我的(第一)问题是编写 param
函数的条件部分(根据 f()
做什么)的最有效方法是什么。我尝试了一些我知道的选项:
if-else if
:
static int f_3(int const param) {
if (A::_1 == param) {
return A::_1 * param;
} else if (A::_2 == param) {
return A::_2 * param;
} // ...
}
switch-case
:
static int f(int const param) {
switch (param) {
case A::_1:
return param * A::_1;
// ...
}
}
goto-label
:
static int f_2(int const param) {
void constexpr* const _table[] = {
&& L1,// ...
};
goto* _table[param];
L1:
return A::_1 * param;
// ...
}
我对它进行了基准测试:
- The complete code.
- 编译器:g++ (GCC) 10.2.0
- 编译器选项:-O3 -std=c++17 -lboost_timer
- 操作系统:ArchLinux 5.10.8-arch1-1
int main(int argc,char* argv[]) {
using ufast_t = std::uint_fast64_t;
ufast_t constexpr n = 1'000'000'000ULL;
ufast_t sum = 0;
{
std::cout << "switch-case :";
boost::timer::auto_cpu_timer _f;
for (ufast_t i = 0; i < n; ++i) {
sum += f(argc);
}
}
std::cout << "---------------------------" << std::endl;
{
std::cout << "goto :";
boost::timer::auto_cpu_timer _f2;
for (ufast_t i = 0; i < n; ++i) {
sum += f_2(argc);
}
}
std::cout << "---------------------------" << std::endl;
{
std::cout << "if-elseif :";
boost::timer::auto_cpu_timer _f3;
for (ufast_t i = 0; i < n; ++i) {
sum += f_3(argc);
}
}
std::cout << std::endl;
return sum;
}
> ./a.out 1 2 3 4 5 6
switch-case : 1.056976s wall,1.050000s user + 0.000000s system = 1.050000s cpu (99.3%)
---------------------------
goto : 0.000001s wall,0.000000s user + 0.000000s system = 0.000000s cpu (n/a%)
---------------------------
if-elseif : 0.645751s wall,0.640000s user + 0.000000s system = 0.640000s cpu (99.1%)
> echo $?
0
那么有什么(有效的)方法可以做到这一点吗?或者我手头只有这些选项,我必须根据一些基准选择其中之一?
第二个问题:
为什么上述基准中的 goto
没有任何开销?这是未定义的行为代码?或者编译器在优化阶段以某种方式删除了它们的循环?
解决方法
我不认为这 3 种不同的方法应该彼此有很大不同,因为编译器可能会识别您在做什么并做最好的事情。最易读的变体是 switch
,但这是基于意见的。它也应该作为汇编程序发出一个跳转表,这应该是最快的选择。但只要相信编译器就行了。
最好将支票移出循环。我可以想象编译器将能够识别出 param 不会改变并移动检出本身,但这不太确定。为了确保,您可以执行以下操作:
template<A param>
auto foo() {
// This function will be generated once for each enumerator of A that is included
// in the switch below. In each variant the runtime code will not contain these
// checks against param.
if constexpr(param == A::_1) {
} else if constexpr(param == A::_2) {
} ...
}
template<A param>
auto loop() {
for(auto i = 0ul; i < 1'000'000; ++i) {
foo<param>();
}
}
int main(int argc,char** argv) {
switch(argc) {
case A::_1:
loop<A::_1>();
break;
....
}
}
遗憾的是,据我所知,没有打开 argc 的好方法。