从捕获 constexpr 函数 return 值的变量中删除 constexpr 会删除编译时评估
removing constexpr from a variable capturing a constexpr function return value removes compile-time evaluation
考虑以下 constexpr
函数 static_strcmp
,它使用 C++17 的 constexpr
char_traits::compare
函数:
#include <string>
constexpr bool static_strcmp(char const *a, char const *b)
{
return std::char_traits<char>::compare(a, b,
std::char_traits<char>::length(a)) == 0;
}
int main()
{
constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
constexpr const char *b = "abc";
constexpr bool result = static_strcmp(a, b);
return result;
}
godbolt 表明这在编译时得到评估,并优化为:
main:
xor eax, eax
ret
从 bool result
中删除 constexpr
:
如果我们从 constexpr bool result
中删除 constexpr
,现在调用将不再优化。
#include <string>
constexpr bool static_strcmp(char const *a, char const *b)
{
return std::char_traits<char>::compare(a, b,
std::char_traits<char>::length(a)) == 0;
}
int main()
{
constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
constexpr const char *b = "abc";
bool result = static_strcmp(a, b); // <-- note no constexpr
return result;
}
godbolt 显示我们现在调用 memcmp
:
.LC0:
.string "abc"
.LC1:
.string "abcdefghijklmnopqrstuvwxyz"
main:
sub rsp, 8
mov edx, 26
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:.LC1
call memcmp
test eax, eax
sete al
add rsp, 8
movzx eax, al
ret
添加短路length
检查:
如果我们首先比较 char_traits::length
中 static_strcmp
中的两个参数,然后 调用 char_traits::compare
, 没有 constexpr
on bool result
,调用再次优化。
#include <string>
constexpr bool static_strcmp(char const *a, char const *b)
{
return
std::char_traits<char>::length(a) == std::char_traits<char>::length(b)
&& std::char_traits<char>::compare(a, b,
std::char_traits<char>::length(a)) == 0;
}
int main()
{
constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
constexpr const char *b = "abc";
bool result = static_strcmp(a, b); // <-- note still no constexpr!
return result;
}
godbolt 表明我们回到了正在优化的调用:
main:
xor eax, eax
ret
- 为什么从对
static_strcmp
的初始调用中删除 constexpr
会导致常量计算失败?
- 很明显,即使 没有
constexpr
,对 char_traits::length
的调用也是在编译时评估的,所以为什么没有 constexpr
的行为不一样在 static_strcmp
? 的第一个版本中
请注意,标准 中没有明确要求 要求在编译时调用 constexpr
函数,请参阅最新草案中的 9.1.5.7:
A call to a constexpr function produces the same result as a call to
an equivalent non-constexpr function in all respects except that (7.1)
a call to a constexpr function can appear in a constant expression and
(7.2) copy elision is not performed in a constant expression
([class.copy.elision]).
(强调我的)
现在,当调用出现在常量表达式中时,编译器无法在编译时避免 运行 该函数,因此它尽职尽责。如果没有(如在您的第二个代码段中),则只是缺少优化的情况。身边不缺人。
我们有三个工作案例:
1) 需要计算值来初始化 constexpr
值或严格要求编译时已知值的地方(非类型模板参数、C 样式数组的大小、测试在 static_assert()
, ...)
2) constexpr
函数使用编译时未知的值(例如:从标准输入接收的值。
3) constexpr
函数接收编译时已知的值,但结果放在编译时不需要的地方。
如果我们忽略 as-if 规则,我们有:
在情况 (1) 中,编译器 必须 计算值编译时,因为计算值是编译时所必需的
在情况 (2) 中,编译器 必须 计算值 运行-time 因为不可能在编译时计算它
在情况 (3) 中,我们处于灰色区域,编译器可以在编译时计算值,但计算值并不是编译时严格要求的;在这种情况下,编译器可以选择是计算编译时还是 运行-time.
用初始代码
constexpr bool result = static_strcmp(a, b);
您属于情况 (1):编译器 必须 计算编译时间,因为 result
变量声明为 constexpr
.
删除 constexpr
、
bool result = static_strcmp(a, b); // no more constexpr
您的代码在灰色区域(案例 (3))中进行翻译,其中可以进行编译时计算但并非严格要求,因为输入值是已知的编译时间(a
和 b
) 但结果
转到不需要编译时值的地方(普通变量)。所以编译器可以选择,在你的情况下,选择 运行-time computation with a version of the function, compile-time computation with another version.
你的程序有未定义的行为,因为你总是比较 strlen(a)
个字符。字符串 b
没有那么多字符。
如果您将字符串修改为等长(这样您的程序就会定义明确),您的程序将如您所愿optimised。
所以这不是遗漏优化。编译器会优化您的程序,但因为它包含未定义的行为,所以不会对其进行优化。
注意,是否是未定义行为,不是很清楚。考虑到编译器使用 memcmp
,它认为两个输入字符串必须至少 strlen(a)
长。所以根据编译器的行为,是undefined behavior。
以下是当前标准草案关于比较的内容:
Returns: 0 if for each i in [0, n), X::eq(p[i],q[i]) is true
; else, a negative value if, for some j in [0, n), X::lt(p[j],q[j]) is true
and for each i in [0, j) X::eq(p[i],q[i]) is true
; else a positive value.
现在不指定compare
是否允许读取p[j+1..n)
或q[j+1..n)
(其中j
是第一个差异的索引)。
考虑以下 constexpr
函数 static_strcmp
,它使用 C++17 的 constexpr
char_traits::compare
函数:
#include <string>
constexpr bool static_strcmp(char const *a, char const *b)
{
return std::char_traits<char>::compare(a, b,
std::char_traits<char>::length(a)) == 0;
}
int main()
{
constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
constexpr const char *b = "abc";
constexpr bool result = static_strcmp(a, b);
return result;
}
godbolt 表明这在编译时得到评估,并优化为:
main: xor eax, eax ret
从 bool result
中删除 constexpr
:
如果我们从 constexpr bool result
中删除 constexpr
,现在调用将不再优化。
#include <string>
constexpr bool static_strcmp(char const *a, char const *b)
{
return std::char_traits<char>::compare(a, b,
std::char_traits<char>::length(a)) == 0;
}
int main()
{
constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
constexpr const char *b = "abc";
bool result = static_strcmp(a, b); // <-- note no constexpr
return result;
}
godbolt 显示我们现在调用 memcmp
:
.LC0: .string "abc" .LC1: .string "abcdefghijklmnopqrstuvwxyz" main: sub rsp, 8 mov edx, 26 mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:.LC1 call memcmp test eax, eax sete al add rsp, 8 movzx eax, al ret
添加短路length
检查:
如果我们首先比较 char_traits::length
中 static_strcmp
中的两个参数,然后 调用 char_traits::compare
, 没有 constexpr
on bool result
,调用再次优化。
#include <string>
constexpr bool static_strcmp(char const *a, char const *b)
{
return
std::char_traits<char>::length(a) == std::char_traits<char>::length(b)
&& std::char_traits<char>::compare(a, b,
std::char_traits<char>::length(a)) == 0;
}
int main()
{
constexpr const char *a = "abcdefghijklmnopqrstuvwxyz";
constexpr const char *b = "abc";
bool result = static_strcmp(a, b); // <-- note still no constexpr!
return result;
}
godbolt 表明我们回到了正在优化的调用:
main: xor eax, eax ret
- 为什么从对
static_strcmp
的初始调用中删除constexpr
会导致常量计算失败? - 很明显,即使 没有
constexpr
,对char_traits::length
的调用也是在编译时评估的,所以为什么没有constexpr
的行为不一样在static_strcmp
? 的第一个版本中
请注意,标准 中没有明确要求 要求在编译时调用 constexpr
函数,请参阅最新草案中的 9.1.5.7:
A call to a constexpr function produces the same result as a call to an equivalent non-constexpr function in all respects except that (7.1) a call to a constexpr function can appear in a constant expression and (7.2) copy elision is not performed in a constant expression ([class.copy.elision]).
(强调我的)
现在,当调用出现在常量表达式中时,编译器无法在编译时避免 运行 该函数,因此它尽职尽责。如果没有(如在您的第二个代码段中),则只是缺少优化的情况。身边不缺人。
我们有三个工作案例:
1) 需要计算值来初始化 constexpr
值或严格要求编译时已知值的地方(非类型模板参数、C 样式数组的大小、测试在 static_assert()
, ...)
2) constexpr
函数使用编译时未知的值(例如:从标准输入接收的值。
3) constexpr
函数接收编译时已知的值,但结果放在编译时不需要的地方。
如果我们忽略 as-if 规则,我们有:
在情况 (1) 中,编译器 必须 计算值编译时,因为计算值是编译时所必需的
在情况 (2) 中,编译器 必须 计算值 运行-time 因为不可能在编译时计算它
在情况 (3) 中,我们处于灰色区域,编译器可以在编译时计算值,但计算值并不是编译时严格要求的;在这种情况下,编译器可以选择是计算编译时还是 运行-time.
用初始代码
constexpr bool result = static_strcmp(a, b);
您属于情况 (1):编译器 必须 计算编译时间,因为 result
变量声明为 constexpr
.
删除 constexpr
、
bool result = static_strcmp(a, b); // no more constexpr
您的代码在灰色区域(案例 (3))中进行翻译,其中可以进行编译时计算但并非严格要求,因为输入值是已知的编译时间(a
和 b
) 但结果
转到不需要编译时值的地方(普通变量)。所以编译器可以选择,在你的情况下,选择 运行-time computation with a version of the function, compile-time computation with another version.
你的程序有未定义的行为,因为你总是比较 strlen(a)
个字符。字符串 b
没有那么多字符。
如果您将字符串修改为等长(这样您的程序就会定义明确),您的程序将如您所愿optimised。
所以这不是遗漏优化。编译器会优化您的程序,但因为它包含未定义的行为,所以不会对其进行优化。
注意,是否是未定义行为,不是很清楚。考虑到编译器使用 memcmp
,它认为两个输入字符串必须至少 strlen(a)
长。所以根据编译器的行为,是undefined behavior。
以下是当前标准草案关于比较的内容:
Returns: 0 if for each i in [0, n), X::eq(p[i],q[i]) is
true
; else, a negative value if, for some j in [0, n), X::lt(p[j],q[j]) istrue
and for each i in [0, j) X::eq(p[i],q[i]) istrue
; else a positive value.
现在不指定compare
是否允许读取p[j+1..n)
或q[j+1..n)
(其中j
是第一个差异的索引)。