布尔表达式是否像使用 if 或 switch 进行分支一样繁琐?

Is a boolean expression as onerous as branching with if or switch?

本文关键字:一样 分支 是否 if switch 布尔表达式      更新时间:2023-10-16

我经常将一些if语句转换为布尔表达式以实现代码紧凑性。例如,如果我有类似的东西

foo(int x)
{
if (x > 5) return 100 + 5;
return 100;
}

我会这样做

foo(int x)
{
return 100 + (x > 5) * 5;
}

这很简单,所以没问题,问题是当我有多个测试时,我可以大大简化它们(以牺牲可读性为代价,但这是一个不同的问题)。

所以问题是,这种(x > 5)评估是否像明确分支一样繁琐。

在这两种情况下,如果表达式的计算结果为true,则必须检查表达式(x > 5)。如前所述,即使未启用任何优化,两个版本也可以编译为同一程序集。

但是,C++核心指南的哲学部分有以下两条规则,您最好注意:

  • P.1:直接在代码中表达想法
  • P.3:表达意图

尽管无论如何都无法强制执行这些规则,但遵守它们将使您采用带有if语句的版本。

这样做将使那些必须在您之后甚至几个月后自己维护代码的人不那么繁重。

您似乎将C++语言结构与程序集中的模式混为一谈。考虑到八十年代末或九十年代初的编译器,在这个级别上对代码进行推理可能是可行的。然而,在这一点上,编译器应用了许多优化和转换,其正确性或实用性对普通程序员来说甚至不明显。一个非常简单的例子是初学者假设以下等价的常见错误:

std::uint16_t a = ...;
a *= 2;   // a multiplication in assembly
a *= 17;  // ditto
a /= 3;   // a division in assembly

然后,他们可能会惊讶地发现他们选择的编译器将这些转换为等效的汇编,例如:

a <<= 1u;
a = (a << 4u) + a; // or even (a << 4u) | a if a < 16
a *= 43691u;

请注意,仅当已知a是除数的倍数时,才允许进行最后一次变换,因此您可能不会经常看到这种优化。它是如何工作的?在数学术语中,uint16_t可以被认为是残余类环Z/(2^16)Z,在这个环中,对于任何与2^16互质的元素(即不能被2整除)都存在乘法逆。如果d(例如 3)是 2 的互质,则它具有这样的逆数,如果已知余数为零,则除以d简直等于乘以d的倒数。(我不会在这里讨论如何计算这种逆数。

这是另一个令人惊讶的优化:

long arithsum(long n)
{
long result = 0;
for (long i=0; i<=n; ++i)
result += i;
return result;
}

GCC 与-O3相当平凡地将其转化为一个展开的添加循环。但是,如果你这样做,我的 Clang 版本(9.0.0svn-something)会在你身上拉一个高斯,并将其转换为类似的东西:

long arithsum(long n)
{
return (n * (n+1)) >> 1;
}

无论如何,同样的警告适用于if/switch等——虽然这些是控制流结构,所以你会认为它们对应于分支,但事实可能并非如此。同样,如果编译器具有优化规则,这似乎是有益的,或者即使它只是无法使用分支(在给定的体系结构上)无法将自己的 AST 或中间表示转换为机器代码,则看似非分支操作的内容可能会转换为分支操作。

TL;DR:在你试图超越你的编译器之前,首先要弄清楚编译器为简单/可读的代码生成的程序集。如果这个程序集很好,那么使代码更微妙/可读性更差是没有意义的。

假设你的意思是 1/0。当然,由于隐式类型转换,它可能在 C/C++ 中工作,但可能不适用于其他语言。如果这是你想要实现的,为什么不使用三元运算符(? :),这也使代码更具可读性

foo(int x) {
return (x > 5) ? (100 + 5) : 100;
}

另请阅读这篇 stackoverflow 文章 -- bool 到 int 的转换