You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++中for循环拷贝std::string为何远慢于其他方式?

为何std::string的operator[]循环拷贝远慢于赋值拷贝及其他类型?

测试背景

我们做了一组基准测试,其中以下两种std::string循环拷贝实现速度极慢:

static void str_loop(benchmark::State& state) {
  std::string   a(N,'a'), b(N,'b'); 
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    for(size_t i=0;i<N;i++) a[i]=b[i];
    benchmark::DoNotOptimize(a);benchmark::DoNotOptimize(b);
  }
}
BENCHMARK(str_loop);

static void str_loop_data(benchmark::State& state) {
  std::string   a(N,'a'), b(N,'b'); 
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    for(size_t i=0;i<N;i++) a.data()[i]=b.data()[i];
    benchmark::DoNotOptimize(a);benchmark::DoNotOptimize(b);
  }
}
BENCHMARK(str_loop_data);

而其余实现速度接近且快得多,包括std::string赋值拷贝、std::u8string的各类操作、std::vector<char>的循环拷贝、以及先把std::string的data()存到指针再循环的版本:

static void str_assign(benchmark::State& state) {
  std::string   a(N,'a'), b(N,'b'); 
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    a=b;
    benchmark::DoNotOptimize(a);benchmark::DoNotOptimize(b);
  }
}
BENCHMARK(str_assign);

static void u8str_assign(benchmark::State& state) {
  std::u8string c(N,'c'), d(N,'d');
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    c=d;
    benchmark::DoNotOptimize(c);benchmark::DoNotOptimize(d);
  }
}
BENCHMARK(u8str_assign);

static void str_loop_ptr(benchmark::State& state) {
  std::string   a(N,'a'), b(N,'b'); 
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    char *dst = a.data();
    char *src = b.data();
    for(size_t i=0;i<N;i++) dst[i]=src[i];
    benchmark::DoNotOptimize(a);benchmark::DoNotOptimize(b);
  }
}
BENCHMARK(str_loop_ptr);

static void u8str_loop_ptr(benchmark::State& state) {
  std::u8string c(N,'c'), d(N,'d');
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    auto *dst = c.data();
    auto *src = d.data();
    for(size_t i=0;i<N;i++) dst[i]=src[i];
    benchmark::DoNotOptimize(c);benchmark::DoNotOptimize(d);
  }
}
BENCHMARK(u8str_loop_ptr);

static void vec_loop(benchmark::State& state) {
  std::vector<char> e(N,'e'),f(N,'f');
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    for(size_t i=0;i<N;i++) e[i]=f[i];
    benchmark::DoNotOptimize(e);benchmark::DoNotOptimize(f);
  }
}
BENCHMARK(vec_loop);

static void u8str_loop(benchmark::State& state) {
  std::u8string c(N,'c'), d(N,'d');
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    for(size_t i=0;i<N;i++) c[i]=d[i];
    benchmark::DoNotOptimize(c);benchmark::DoNotOptimize(d);
  }
}
BENCHMARK(u8str_loop);

static void vec_loop_data(benchmark::State& state) {
  std::vector<char> e(N,'e'),f(N,'f');
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    for(size_t i=0;i<N;i++) e.data()[i]=f.data()[i];
    benchmark::DoNotOptimize(e);benchmark::DoNotOptimize(f);
  }
}
BENCHMARK(vec_loop_data);

static void u8str_loop_data(benchmark::State& state) {
  std::u8string c(N,'c'), d(N,'d');
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    for(size_t i=0;i<N;i++) c.data()[i]=d.data()[i];
    benchmark::DoNotOptimize(c);benchmark::DoNotOptimize(d);
  }
}
BENCHMARK(u8str_loop_data);

static void str_memcpy(benchmark::State& state) {
  std::string   a(N,'a'), b(N,'b'); 
  // 循环前代码不参与性能统计
  for (auto _ : state) {
    memcpy(a.data(),b.data(),N*sizeof(a[0]));
    benchmark::DoNotOptimize(a);benchmark::DoNotOptimize(b);
  }
}
BENCHMARK(str_memcpy);

基准测试结果截图:
基准测试截图

原因分析

核心问题确实和严格别名规则有关:

  1. 多数标准库的std::string实现会采用小字符串优化(SSO),将短字符串直接存在对象内部的缓冲区中,而长字符串则分配在堆上。部分实现会用union来存储内部缓冲区或堆指针,这就引入了别名歧义。
  2. 当循环中每次调用a[i]或a.data()时,编译器无法确定这些操作是否会修改std::string对象本身的元数据(比如SSO切换时的指针变更)。受严格别名规则限制,编译器不敢假设a[i]的访问不会和a的对象状态产生冲突,因此无法将循环优化为高效的批量内存拷贝(如memcpy),只能生成逐字节拷贝的低效代码。
  3. std::u8string的实现通常没有这种复杂的别名逻辑,或者编译器能更明确地推断其内部缓冲区的类型,因此可以正常优化循环。而std::string的赋值运算符operator=是库内部实现,编译器能直接识别并优化为批量拷贝,不存在循环中的别名歧义。

另外,提前将a.data()缓存到独立的char*变量时,相当于给编译器明确的提示:该指针仅指向数据缓冲区,不会和std::string对象的元数据产生别名冲突,因此编译器可以放心地将循环优化为批量拷贝。

解决方法

要避免这种性能损耗,可采用以下几种方式:

  • 提前缓存数据指针:像str_loop_ptr那样,先将a.data()和b.data()赋值给独立的char*变量,再用指针进行循环拷贝,消除别名歧义。
  • 优先使用赋值运算符:如果是整个字符串拷贝,直接用a = b即可,库实现本身已经做了最优处理。
  • 手动调用memcpy:明确用memcpy(a.data(), b.data(), N)替代循环,直接告诉编译器执行批量内存拷贝。
  • 使用编译器别名提示:如果必须保留operator[]循环,可使用__restrict(GCC/Clang)或__declspec(restrict)(MSVC)修饰指针,告知编译器这些指针无别名:
    static void str_loop_restrict(benchmark::State& state) {
      std::string   a(N,'a'), b(N,'b'); 
      for (auto _ : state) {
        char* __restrict dst = a.data();
        const char* __restrict src = b.data();
        for(size_t i=0;i<N;i++) dst[i]=src[i];
        benchmark::DoNotOptimize(a);benchmark::DoNotOptimize(b);
      }
    }
    BENCHMARK(str_loop_restrict);
    

内容的提问来源于stack exchange,提问作者jerryc05

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 08:47:36