字符串分割性能对比:string_view 不是可选的
字符串分割是文本处理中最常见的操作之一。C++ 提供了多种实现方式,而返回类型的选择——std::string 还是 std::string_view——比具体算法的影响更大。本基准测试使用 Google Benchmark 对 11 种实现进行了对比。
实现方式
测试了三个设计维度:
- 返回类型:
std::vector<std::string>(需要分配)vsstd::vector<std::string_view>(零拷贝) - 迭代风格:基于索引(
find_first_of)、基于迭代器(std::find_first_of)、基于原始指针 - 库:手写 vs Abseil
StrSplit
手写实现
基于索引(返回 string):
std::vector<std::string> split(const std::string& str,
const std::string& delims = " ") {
std::vector<std::string> output;
size_t first = 0;
while (first < str.size()) {
const auto second = str.find_first_of(delims, first);
if (first != second)
output.emplace_back(str.substr(first, second - first));
if (second == std::string::npos) break;
first = second + 1;
}
return output;
}
基于指针(返回 string_view):
std::vector<std::string_view> splitSVPtr(std::string_view str,
std::string_view delims = " ") {
std::vector<std::string_view> output;
for (auto first = str.data(), second = str.data(), last = first + str.size();
second != last && first != last;
first = second + 1) {
second = std::find_first_of(first, last,
std::cbegin(delims), std::cend(delims));
if (first != second)
output.emplace_back(first, second - first);
}
return output;
}
单字符分隔符(string_view)
最快的实现将分隔符限制为单个字符:
std::vector<std::string_view> SplitByChar(std::string_view s, char delim = ' ') {
std::vector<std::string_view> out;
out.reserve(s.size() / 4 + 2);
size_t start = 0;
for (size_t i = 0; i <= s.size(); ++i) {
if (i == s.size() || s[i] == delim) {
out.emplace_back(s.data() + start, i - start);
start = i + 1;
}
}
return out;
}
此版本始终输出空字段(类似保留了空值的 strtok),适用于 CSV 解析等场景。
Abseil
// 返回 std::vector<std::string>
auto v = absl::StrSplit(str, absl::ByAnyChar(delim));
// 返回 std::vector<absl::string_view>
auto v = absl::StrSplit(strv, absl::ByAnyChar(delim));
// 单字符(最快的 Abseil 变体)
auto v = absl::StrSplit(strv, delim);
结果
输入:一段约 400 字符的 Lorem Ipsum 文本。数值越低越好。
| 实现 | 返回类型 | 耗时 (ns) | 相对基准 |
|---|---|---|---|
split | vector<string> | 3,042 | 基准 |
splitCYB | vector<string> | 3,486 | 慢 1.15 倍 |
splitAbseil | vector<string> | 2,890 | 快 1.05 倍 |
splitStd | vector<string> | 1,473 | 快 2.1 倍 |
splitPtr | vector<string> | 1,406 | 快 2.2 倍 |
splitAbseilSV | vector<sv> | 2,487 | 快 1.2 倍 |
splitAbseilChar | vector<sv> | 807 | 快 3.8 倍 |
splitSV | vector<sv> | 585 | 快 5.2 倍 |
splitSVStd | vector<sv> | 540 | 快 5.6 倍 |
splitSVPtr | vector<sv> | 538 | 快 5.7 倍 |
SplitByChar | vector<sv> | 390 | 快 7.8 倍 |
分析
返回类型是决定性因素
每个返回 string_view 的实现都优于返回 string 的实现,与迭代策略无关。最快的 string 返回变体(splitPtr,1406 ns)仍然比 string_view 变体慢得多。
string_view 为什么快
在返回 string 的版本中,每次调用 output.emplace_back(str.substr(...)) 都会为每个 token 分配堆内存。一个 40 个单词的段落每次调用创建约 40 次堆分配。string_view 版本只需为 vector 的内部缓冲区分配一次,然后存储指向原始字符串的指针。
单字符的优势
SplitByChar 不仅更简单——它更快,因为比较单个字符(s[i] == delim)比在分隔符集中搜索任意字符(find_first_of)更便宜。当分隔符在编译时已知为单个字符时,应该利用这个优势。
Abseil 多字符版本出人意料地慢
splitAbseilSV(2487 ns)比 splitSVPtr(538 ns)慢 4.6 倍,尽管两者都返回 string_view。这可能是因为 Abseil 的 ByAnyChar 路径每个 token 有更多开销(分隔符集构造、迭代器间接引用),相比之下手写的简单循环更轻量。
单字符 Abseil 变体(splitAbseilChar,807 ns)好很多,但仍比手写的 SplitByChar 慢 2 倍。Abseil 分割机制的抽象成本不容忽视。
建议
- 除非需要修改或持有 token,否则返回
string_view。 仅减少分配就能带来 5 倍加速。 - 当分隔符是单个字符时,使用单字符实现。 它是最快且最简单的方案。
- 如果需要多字符分隔符,基于指针的
find_first_of配合string_view是最快的。 - Abseil 的
StrSplit适用于可读性比最后 2 倍性能更重要的通用场景。尽可能使用单字符重载。 - 预分配输出 vector。 预留
s.size() / 4 + 2个槽位(如SplitByChar所做)避免了增长过程中的重新分配。