string_view::find_first_of 错过了优化
Missed optimization with string_view::find_first_of
更新: 相关 GCC 错误报告:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=103798
我测试了以下代码:
#include <string_view>
size_t findFirstE_slow(std::string_view sv) {
return sv.find_first_of("eE");
}
size_t findFirstE_fast(std::string_view sv) {
auto it{sv.begin()};
for (; it != sv.end() && *it != 'e' && *it != 'E'; ++it)
;
return it == sv.end() ? std::string_view::npos : size_t(it - sv.begin());
}
快速基准测试:https://quick-bench.com/q/dSU3EBzI8MtGOFn_WLpK3ErT3ok
编译器资源管理器输出:https://godbolt.org/z/eW3sx61vz
findFirstE_slow()
和 firstFirstE_fast()
函数的目的是做同样的事情,但 findFirstE_slow()
运行速度明显较慢(在快速基准测试中至少慢 5 倍)。
这是 x86-64 gcc (trunk) -std=c++20 -O3
.
的汇编输出
findFirstE_slow():
.LC0:
.string "eE"
findFirstE_slow(std::basic_string_view<char, std::char_traits<char> >):
push r12
push rbp
push rbx
test rdi, rdi
je .L4
mov rbx, rdi
mov rbp, rsi
xor r12d, r12d
jmp .L3
.L8:
add r12, 1
cmp rbx, r12
je .L4
.L3:
movsx esi, BYTE PTR [rbp+0+r12]
mov edx, 2
mov edi, OFFSET FLAT:.LC0
call memchr
test rax, rax
je .L8
mov rax, r12
pop rbx
pop rbp
pop r12
ret
.L4:
mov r12, -1
pop rbx
pop rbp
mov rax, r12
pop r12
ret
findFirstE_fast():
findFirstE_fast(std::basic_string_view<char, std::char_traits<char> >):
add rdi, rsi
cmp rdi, rsi
je .L13
mov rax, rsi
jmp .L12
.L15:
add rax, 1
cmp rdi, rax
je .L13
.L12:
movzx edx, BYTE PTR [rax]
and edx, -33
cmp dl, 69
jne .L15
sub rax, rsi
ret
.L13:
mov rax, -1
ret
有趣的是,findFirstE_slow()
为 sv
中的每个字符调用 memchr("eE", *current_char, 2)
。
另一方面,findFirstE_fast()
通过将 sv
中的每个字符与 'e' 和 'E'.
进行比较,实现了我们合理预期的效果
Clang 生成类似的输出。
问题:像我测试中的短字符串,这里是否有遗漏的优化?我是否缺少让 GCC 生成更快代码的东西?
libstdc++ 的 std::string_view::find_first_of
看起来像:
size_type find_first_of(std::string_view v, std::size_t pos = 0) {
if (v.empty()) return npos;
for (; pos < size(); ++pos) {
const char_type* p = traits_type::find(v.data(), v.size(), this->data()[pos]);
if (p) return pos;
}
return npos;
}
你可以看到 traits_type::find
是如何转化为 memchr
的。
问题的症结在于 memchr("eE", this->data()[pos], 2) != nullptr
的编译方式与 this->data()[pos] == 'e' || this->data()[pos] == 'E'
不同,尽管后者效率更高。
你可以通过尝试编译这个来检查:
constexpr unsigned char characters[] = "eE";
bool a(unsigned char* p) {
return __builtin_memchr(characters, *p, 2);
}
bool b(unsigned char* p) {
return *p == characters[0] || *p == characters[1];
}
这是一个错过的优化,但您可以提示编译器不要将 memchr 与自定义特征类型一起使用:
struct char_traits : std::char_traits<char> {
static constexpr const char_type* find(const char_type* p, std::size_t count, const char_type& ch) {
if (__builtin_constant_p(count) && count < 5) {
switch (count) {
case 0: return nullptr;
case 1: return ch == *p ? p : nullptr;
case 2: return ch == *p ? p : ch == *++p ? p : nullptr;
case 3: return ch == *p ? p : ch == *++p ? p : ch == *++p ? p : nullptr;
case 4: return ch == *p ? p : ch == *++p ? p : ch == *++p ? p : ch == *++p ? p : nullptr;
}
}
return std::char_traits<char>::find(p, count, ch);
}
};
using string_view = std::basic_string_view<char, char_traits>;
size_t findFirstE_slow(string_view sv) {
return sv.find_first_of(characters);
}
// Also your "fast" version needs to return
// return it == sv.end() ? string_view::npos : size_t(it - sv.begin());
// to be equivalent
(https://godbolt.org/z/bhPPxjboE)
和 https://quick-bench.com/q/QVxVTxGEagUUCPuhFi9T8wjI1qQ says the slow version is now only 1.3x slower. Using a larger string (https://quick-bench.com/q/el0ukDywBNMoGsEb33PM_g4WUaY; 'e'
之前的 8000 个字符),差异几乎不明显。
现在的主要区别是一个遍历索引,另一个遍历指针(最后返回差异)。汇编中的两个不同指令是 movzx edx, BYTE PTR [rsi+rax]
和 movzx edx, BYTE PTR [rax]
sub rax, rsi
,您应该会发现第二个版本稍微快一点(尤其是渐近的,因为减法发生在循环之外)
更新: 相关 GCC 错误报告:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=103798
我测试了以下代码:
#include <string_view>
size_t findFirstE_slow(std::string_view sv) {
return sv.find_first_of("eE");
}
size_t findFirstE_fast(std::string_view sv) {
auto it{sv.begin()};
for (; it != sv.end() && *it != 'e' && *it != 'E'; ++it)
;
return it == sv.end() ? std::string_view::npos : size_t(it - sv.begin());
}
快速基准测试:https://quick-bench.com/q/dSU3EBzI8MtGOFn_WLpK3ErT3ok
编译器资源管理器输出:https://godbolt.org/z/eW3sx61vz
findFirstE_slow()
和 firstFirstE_fast()
函数的目的是做同样的事情,但 findFirstE_slow()
运行速度明显较慢(在快速基准测试中至少慢 5 倍)。
这是 x86-64 gcc (trunk) -std=c++20 -O3
.
findFirstE_slow():
.LC0:
.string "eE"
findFirstE_slow(std::basic_string_view<char, std::char_traits<char> >):
push r12
push rbp
push rbx
test rdi, rdi
je .L4
mov rbx, rdi
mov rbp, rsi
xor r12d, r12d
jmp .L3
.L8:
add r12, 1
cmp rbx, r12
je .L4
.L3:
movsx esi, BYTE PTR [rbp+0+r12]
mov edx, 2
mov edi, OFFSET FLAT:.LC0
call memchr
test rax, rax
je .L8
mov rax, r12
pop rbx
pop rbp
pop r12
ret
.L4:
mov r12, -1
pop rbx
pop rbp
mov rax, r12
pop r12
ret
findFirstE_fast():
findFirstE_fast(std::basic_string_view<char, std::char_traits<char> >):
add rdi, rsi
cmp rdi, rsi
je .L13
mov rax, rsi
jmp .L12
.L15:
add rax, 1
cmp rdi, rax
je .L13
.L12:
movzx edx, BYTE PTR [rax]
and edx, -33
cmp dl, 69
jne .L15
sub rax, rsi
ret
.L13:
mov rax, -1
ret
有趣的是,findFirstE_slow()
为 sv
中的每个字符调用 memchr("eE", *current_char, 2)
。
另一方面,findFirstE_fast()
通过将 sv
中的每个字符与 'e' 和 'E'.
Clang 生成类似的输出。
问题:像我测试中的短字符串,这里是否有遗漏的优化?我是否缺少让 GCC 生成更快代码的东西?
libstdc++ 的 std::string_view::find_first_of
看起来像:
size_type find_first_of(std::string_view v, std::size_t pos = 0) {
if (v.empty()) return npos;
for (; pos < size(); ++pos) {
const char_type* p = traits_type::find(v.data(), v.size(), this->data()[pos]);
if (p) return pos;
}
return npos;
}
你可以看到 traits_type::find
是如何转化为 memchr
的。
问题的症结在于 memchr("eE", this->data()[pos], 2) != nullptr
的编译方式与 this->data()[pos] == 'e' || this->data()[pos] == 'E'
不同,尽管后者效率更高。
你可以通过尝试编译这个来检查:
constexpr unsigned char characters[] = "eE";
bool a(unsigned char* p) {
return __builtin_memchr(characters, *p, 2);
}
bool b(unsigned char* p) {
return *p == characters[0] || *p == characters[1];
}
这是一个错过的优化,但您可以提示编译器不要将 memchr 与自定义特征类型一起使用:
struct char_traits : std::char_traits<char> {
static constexpr const char_type* find(const char_type* p, std::size_t count, const char_type& ch) {
if (__builtin_constant_p(count) && count < 5) {
switch (count) {
case 0: return nullptr;
case 1: return ch == *p ? p : nullptr;
case 2: return ch == *p ? p : ch == *++p ? p : nullptr;
case 3: return ch == *p ? p : ch == *++p ? p : ch == *++p ? p : nullptr;
case 4: return ch == *p ? p : ch == *++p ? p : ch == *++p ? p : ch == *++p ? p : nullptr;
}
}
return std::char_traits<char>::find(p, count, ch);
}
};
using string_view = std::basic_string_view<char, char_traits>;
size_t findFirstE_slow(string_view sv) {
return sv.find_first_of(characters);
}
// Also your "fast" version needs to return
// return it == sv.end() ? string_view::npos : size_t(it - sv.begin());
// to be equivalent
(https://godbolt.org/z/bhPPxjboE)
和 https://quick-bench.com/q/QVxVTxGEagUUCPuhFi9T8wjI1qQ says the slow version is now only 1.3x slower. Using a larger string (https://quick-bench.com/q/el0ukDywBNMoGsEb33PM_g4WUaY; 'e'
之前的 8000 个字符),差异几乎不明显。
现在的主要区别是一个遍历索引,另一个遍历指针(最后返回差异)。汇编中的两个不同指令是 movzx edx, BYTE PTR [rsi+rax]
和 movzx edx, BYTE PTR [rax]
sub rax, rsi
,您应该会发现第二个版本稍微快一点(尤其是渐近的,因为减法发生在循环之外)