【问题标题】:Unexpectedly poor execution time for string concatenation function字符串连接函数的执行时间出乎意料地差
【发布时间】:2018-02-14 17:46:09
【问题描述】:

我编写了以下字符串连接函数 (join) 来减少分配次数和构造最终字符串所花费的时间。我还想写一个易于使用的附加函数(如果可能的话,单行)。

size_t str_size(const char *str) {
    return std::strlen(str);
}

size_t str_size(const std::string &str) {
    return str.size();
}

template <typename T>
size_t accumulated_size(const T& last) {
    return str_size(last);
}

template <typename T, typename... Args>
size_t accumulated_size(const T& first, const Args& ...args) {
    return str_size(first) + accumulated_size(args...);
}

template <typename T>
void append(std::string& final_string, const T &last) {
    final_string += last;
}

template <typename T, typename... Args>
void append(std::string& final_string, const T& first, const Args& ...args) {
    final_string += first;
    append(final_string, args...);
}

template <typename T, typename... Args>
std::string join(const T& first, const Args& ...args) {
    std::string final_string;

    final_string.reserve(accumulated_size(first, args...));
    append(final_string, first, args...);

    return std::move(final_string);
}

我使用operator+=std::string 类的operator+ 在相当大量的字符串上针对典型的内置C++ 连接功能测试了join 方法。与普通的 operator+=operator+ 方法相比,我的方法如何以及为什么在时间执行方面产生更差的结果?

我正在使用以下类来测量时间:

class timer {
public:
    timer() {
        start_ = std::chrono::high_resolution_clock::now();
    }

    ~timer() {
        end_ = std::chrono::high_resolution_clock::now();
        std::cout << "Execution time: " << std::chrono::duration_cast<std::chrono::nanoseconds>(end_ - start_).count() << " ns." << std::endl;
    }

private:
    std::chrono::time_point<std::chrono::high_resolution_clock> start_;
    std::chrono::time_point<std::chrono::high_resolution_clock> end_;
};

我正在比较以下方式:

#define TEST_DATA "Lorem", "ipsum", "dolor", "sit", "ame", "consectetuer", "adipiscing", "eli", "Aenean",\
                    "commodo", "ligula", "eget", "dolo", "Aenean", "mass", "Cum", "sociis", "natoque",\
                    "penatibus", "et", "magnis", "dis", "parturient", "monte", "nascetur", "ridiculus",\
                    "mu", "Donec", "quam", "feli", ", ultricies", "ne", "pellentesque", "e", "pretium",\
                    "qui", "se", "Nulla", "consequat", "massa", "quis", "eni", "Donec", "pede", "just",\
                    "fringilla", "ve", "aliquet", "ne", "vulputate", "ege", "arc", "In", "enim", "just",\
                    "rhoncus", "u", "imperdiet", "", "venenatis", "vita", "just", "Nullam", "ictum",\
                    "felis", "eu", "pede", "mollis", "pretiu", "Integer", "tincidunt"

#define TEST_DATA_2 std::string("Lorem") + "ipsum"+ "dolor"+ "sit"+ "ame"+ "consectetuer"+ "adipiscing"+ "eli"+ "Aenean"+\
                    "commodo"+ "ligula"+ "eget"+ "dolo"+ "Aenean"+ "mass"+ "Cum"+ "sociis"+ "natoque"+\
                    "penatibus"+ "et"+ "magnis"+ "dis"+ "parturient"+ "monte"+ "nascetur"+ "ridiculus"+\
                    "mu"+ "Donec"+ "quam"+ "feli"+ ", ultricies"+ "ne"+ "pellentesque"+ "e"+ "pretium"+\
                    "qui"+ "se"+ "Nulla"+ "consequat"+ "massa"+ "quis"+ "eni"+ "Donec"+ "pede"+ "just"+\
                    "fringilla"+ "ve"+ "aliquet"+ "ne"+ "vulputate"+ "ege"+ "arc"+ "In"+ "enim"+ "just"+\
                    "rhoncus"+ "u"+ "imperdiet"+ ""+ "venenatis"+ "vita"+ "just"+ "Nullam"+ "ictum"+\
                    "felis"+ "eu"+ "pede"+ "mollis"+ "pretiu"+ "Integer"+ "tincidunt"

int main() {
    std::string string_builder_result;
    std::string normal_approach_result_1;
    std::string normal_approach_result_2;

    {
        timer t;
        string_builder_result = join(TEST_DATA);
    }

    std::vector<std::string> vec { TEST_DATA };
    {
        timer t;
        for (const auto & x : vec) {
            normal_approach_result_1 += x;
        }
    }

    {
        timer t;
        normal_approach_result_2 = TEST_DATA_2;
    }
}

我的结果是:

  • 执行时间:11552 ns(join 方法)。
  • 执行时间:3701 ns(operator+=() 方法)。
  • 执行时间:5898 ns(operator+() 方法)。

我正在编译:g++ efficient_string_concatenation.cpp -std=c++11 -O3

【问题讨论】:

  • 请包括您的测试,包括编译器标志和使用的数据。长期以来,性能 C++ 测试中的性能分析都做得很差。
  • 您应该使用 stable_clock,而不是 high_resolution_clock 进行计时。你不是在打开 -O3 的情况下编译吗?
  • 我刚刚用 -O3 编译。它实际上有所改进,但仍不比其他方法好。结果:执行时间:8949 ns。 (加入方法),执行时间:3475 ns。 (操作员 += 方法)
  • 我不是这方面的专家,但您在 append 函数中使用了递归调用。我的猜测是,与在向量上循环相比,它会影响您的性能
  • 避免移动返回的字符串,它会阻止 RVO。参见例如stackoverflow.com/questions/19267408/…

标签: c++ c++11 variadic-templates string-concatenation coding-efficiency


【解决方案1】:

operator+ 在左侧有一个右值引用 std::string。你写的+= 代码根本不比+ 的长链好。

它的+ 可以使用指数重新分配,从 10 左右开始。增长因子为 1.5,即大约 9 次分配和重新分配。

递归可能会使事情变得混乱或减慢速度。你可以解决这个问题:

template <typename... Args>
void append(std::string& final_string, const Args&... args) {
  using unused=int[];
  (void)unused{0,(void(
    final_string += args
  ),0)...};
}

这消除了递归,类似地:

template <typename... Args>
size_t accumulated_size(const Args& ...args) {
  size_t r = 0;
  using unused=int[];
  (void)unused{0,(void(
    r += str_size(args)
  ),0)...};
  return r;
}

但是,可能不值得访问那么多字符串来计算它们的长度以节省 8 次重新分配。

【讨论】:

  • (void)unused={...}; 技巧可以使用 C++17 折叠表达式替换为 ((final_string += args), ...);,对吗?
  • @Bob__ 是的,但这是 C++11
  • 你用来摆脱递归的“技巧”是什么?我对它完全不熟悉,我第一次遇到它。你能指出我可以阅读更多相关信息的资源吗?
  • @Yakk 删除递归并删除关于字符串累积大小的计算会产生或多或少相同的结果。这很奇怪。我预计至少有一个改进可以克服std::string::operator+ 方法的结果。这种当前的技术在我看来与std::string::operator+= 方法基本相同,但仍然会导致执行时间变差。
【解决方案2】:

请不要为此处理字符串。

使用字符串流,或者像这样创建自己的 StringBuilder:https://www.codeproject.com/Articles/647856/Performance-Improvement-with-the-StringBuilde

推荐使用专门的字符串构建器,因为它具有智能分配管理(有时它们支持字符串块列表,有时 - 预测增长)。分配是一项艰巨且非常耗时的操作。

【讨论】:

  • 这个问题是在寻找一个解释,而不仅仅是一个建议。您的回答没有为提问者提供任何见解,可能会被删除。请edit解释导致观察到的症状的原因。
猜你喜欢
  • 2021-08-01
  • 2020-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-11
  • 1970-01-01
  • 2019-12-28
相关资源
最近更新 更多