yuqi-zheng

字符串分割性能对比:string_view 不是可选的


字符串分割是文本处理中最常见的操作之一。C++ 提供了多种实现方式,而返回类型的选择——std::string 还是 std::string_view——比具体算法的影响更大。本基准测试使用 Google Benchmark 对 11 种实现进行了对比。


实现方式

测试了三个设计维度:

  1. 返回类型std::vector<std::string>(需要分配)vs std::vector<std::string_view>(零拷贝)
  2. 迭代风格:基于索引(find_first_of)、基于迭代器(std::find_first_of)、基于原始指针
  3. :手写 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)相对基准
splitvector<string>3,042基准
splitCYBvector<string>3,486慢 1.15 倍
splitAbseilvector<string>2,890快 1.05 倍
splitStdvector<string>1,473快 2.1 倍
splitPtrvector<string>1,406快 2.2 倍
splitAbseilSVvector<sv>2,487快 1.2 倍
splitAbseilCharvector<sv>807快 3.8 倍
splitSVvector<sv>585快 5.2 倍
splitSVStdvector<sv>540快 5.6 倍
splitSVPtrvector<sv>538快 5.7 倍
SplitByCharvector<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 分割机制的抽象成本不容忽视。


建议

  1. 除非需要修改或持有 token,否则返回 string_view 仅减少分配就能带来 5 倍加速。
  2. 当分隔符是单个字符时,使用单字符实现。 它是最快且最简单的方案。
  3. 如果需要多字符分隔符,基于指针的 find_first_of 配合 string_view 是最快的。
  4. Abseil 的 StrSplit 适用于可读性比最后 2 倍性能更重要的通用场景。尽可能使用单字符重载。
  5. 预分配输出 vector。 预留 s.size() / 4 + 2 个槽位(如 SplitByChar 所做)避免了增长过程中的重新分配。