多次分配在C++中有多大开销?

huangapple go评论92阅读模式
英文:

How costly is it to have multiple allocations c++?

问题

以下是要翻译的内容:

我想了解在实用函数中内存分配过程的成本如何。
例如,以下两种实现之间是否有很大的差异?

string method_A(string inp) {
auto num_fillers = inp.length() - 4;
string filler(num_fillers, '-');
string res = filler + inp.substr(num_fillers);
return res;
}

string method_B(string inp) {
const int expect_len = 4;
auto num_fillers = inp.length() - expect_len;
string res;
res.reserve(num_fillers + expect_len);
res.assign(num_fillers, '-');
res += inp.substr(num_fillers);
return res;
}

使用method_B而不是method_A是否有任何好处?
各种方法的优缺点是什么?

内存分配一次并使用"+="操作来填充字符串是否比在method_A中进行的方式性能更好?

英文:

I wonder to learn how costly is the memory allocation process in a utility function.
For example is there a big difference between the following implementations?

string method_A (string inp) {
   auto        num_fillers = inp.length() - 4;
   string filler(num_fillers, '-');
   string res = filler + inp.substr(num_fillers);
   return res;
}

string method_B (string inp) {
  const int expect_len =4;
  auto        num_fillers = inp.length() - expect_len;
  string res;
  res.reserve(num_fillers + expect_len);
  res.assign(num_fillers, '-');
  res+= inp.substr(num_fillers);
  return res;
}

Is there any benefit of using method_B instead of method_A?
What are the Pros and cons of each?

Does memory allocation once and filling up the string with the "+=" operation have a better performance than what it has been done in method_A?

答案1

得分: 5

分配内存确实会产生开销,在实际情况下应尽量避免不必要的内存分配。但不应以维护性和复杂性为代价,除非有必要。因此,您应该始终权衡这一点。

在标准库的情况下,没有realloc等效的函数。向动态容器添加元素通常涉及分配、复制和删除(忽略了小字符串优化和预留容量)。如果在程序中频繁执行这些操作,您可能需要考虑内存碎片化的可能性。

在您的情况下,您执行了相当多的内存分配。

方法A:

  1. string inp 参数按值传递
  2. filler 构造函数分配另一个字符串
  3. inp.substr(num_fillers) 分配另一个字符串
  4. operator+ 分配另一个字符串

方法B(目前存在很多错误):

  1. string inp 参数按值传递
  2. res.reserve(num_fillers + expect_len) 分配另一个字符串
  3. inp.substr(num_fillers) 分配另一个字符串

因此,这并没有好多少。您避免了一个内存分配,但仍然进行了三次分配。具有更少分配的方法(基于您在method_B中尝试的方法)是:

std::string method_C(const std::string& inp)
{
  const std::size_t expect_len = 4;
  const std::size_t num_fillers = inp.size() - std::min(inp.size(), expect_len);
  std::string res;
  res.reserve(num_fillers + expect_len);
  res.assign(num_fillers, '-');
  res.append(inp.begin() + num_fillers, inp.end());
  return res;
}

上述方法仅执行一次分配。

如果您真的喜欢使用substr,可以使用std::string_view的替代方法:

  res += std::string_view(inp).substr(num_fillers);
英文:

Allocations do incur overhead, and it's good to avoid unnecessary ones when practical. But not at the expense of maintainability/complexity unless necessary. So you should always weigh that up.

In the case of the standard library, there is no realloc equivalent. Appending to a dynamic container typically involves an allocation, a copy and a delete (ignoring small string optimizations and pre-reserved capacity). If you are doing this a lot in your program, you may need to consider the possibility of memory fragmentation.

In your case, you are doing quite a lot of allocations.

Method A:

  1. string inp parameter is passed by value
  2. filler constructor allocates another string
  3. inp.substr(num_fillers) allocates another string
  4. operator+ allocates another string

Method B (full of bugs right now):

  1. string inp parameter is passed by value
  2. res.reserve(num_fillers + expect_len) allocates another string
  3. inp.substr(num_fillers) allocates another string

So, it's not a whole lot better. You avoided one allocation but still made three. An approach with fewer allocations (based on your attempt in method_B) would be:

std::string method_C(const std::string& inp)
{
  const std::size_t expect_len = 4;
  const std::size_t num_fillers = inp.size() - std::min(inp.size(), expect_len);
  std::string res;
  res.reserve(num_fillers + expect_len);
  res.assign(num_fillers, '-');
  res.append(inp.begin() + num_fillers, inp.end());
  return res;
}

The above performs only one allocation.

An alternative to the append, if you really like using substr is to use std::string_view instead:

  res += std::string_view(inp).substr(num_fillers);

答案2

得分: 3

当然,较少的分配比更多的分配更高效;另一方面,采用“最高效”的方式(通过预先计算您将需要的最终缓冲区大小,这样您只需分配一次空间)会使您的代码更加复杂和难以维护,并引入了您可能会计算错误缓冲区大小的风险(无论是现在还是在以后修改了字符串生成方式并未正确更新大小计算后),结果可能从性能降低到缓冲区溢出。

因此,您需要权衡最大化运行时效率与保持代码简单和可维护性的重要性。一个好的经验法则是:如果您无法轻松测量性能差异,那么它并不重要,因此您应该选择产生最清晰代码的方法。(即使您可以测量性能差异,也应该问问自己在这个程序中性能是否足够重要,以使额外的复杂性值得。)

英文:

Certainly fewer allocations is more efficient than more allocations; OTOH doing it the "most efficient" way (by precalculating the final buffer-size you'll need in advance, so you only have to allocate the space once) makes your code more complex and harder to maintain, and introduces the risk that you'll miscalculate the buffer-size (either now, or in the future after you've modified how the string is generated and didn't properly update the size-calculation afterwards), with results potentially ranging from reduced performance to a buffer-overrun.

So you'll need to weigh the importance of maximizing runtime efficiency against the importance of keeping the code simple and maintainable. A good rule of thumb is: if you can't easily measure the performance difference, then it doesn't matter, so you should just go with whatever method produces the clearest code. (and even if you can measure a performance difference, you should ask yourself if performance matters enough in this program to make the additional complexity worthwhile)

答案3

得分: 3

你必须理解,你正在编写的C++代码会被编译成机器代码。这个编译步骤可以根据as-if规则进行许多改变。特别是,C++编译器可以以你可能无法预测的方式优化你的代码,除非你对这个主题非常了解。

因此,你不能真正在性能方面比较method_Amethod_B。一个足够好的优化器可以为两者生成相同的机器代码。

这里是你的代码的另一个版本,可能会被编译成相同的机器代码:

string method_C(string inp) {
    const int expect_len = 4;
    auto num_fillers = inp.length() - expect_len;
    inp.replace(0, num_fillers, num_fillers, '-');
    return inp;
}

正如你可以看到的,我去掉了res局部变量。这是否意味着它执行的内存分配较少?不一定。这取决于优化器对它的处理方式。

那么,能做什么呢?如果真的有必要的话,进行性能基准测试(如果你这样做,请学会正确地进行)。否则,编写可读的代码,因为代码不仅仅是为了编译器,更是为了其他人类而编写的。

英文:

You have to understand that you are writing C++ code that gets compiled into machine code. This compilation step can make a lot of changes based on the as-if rule. In particular, a C++ compiler can optimize your code in ways you probably cannot predict unless you are particularly knowledgeable on that topic.

As a result, you cannot really compare method_A and method_B in terms of performance. A sufficiently good optimizer could produce the same machine code for both.

Here is yet another version of your code that could be compiled into the same machine code:

string method_C(string inp) {
    const int expect_len = 4;
    auto num_fillers = inp.length() - expect_len;
    inp.replace(0, num_fillers, num_fillers, '-');
    return inp;
}

As you can see, I got rid of the res local variable. Does that mean it performs less memory allocation? Not necessarily. It depends on what the optimizer does with it.

Now, what can you do about it? Benchmark, if it's really necessary (and if you do, learn to do it correctly). Otherwise, write readable code, because code is written just as much, if not more, for other humans as for compilers.

答案4

得分: -3

你打算进行多少次分配?两次还是三次都没有丝毫区别。一千万次应该是不可察觉的;如果不是的话,那么你的内存分配器很差劲。十亿次(可能还伴随着很多次释放),这才是你测量代码性能的地方,看它是否成为瓶颈,并尝试不同的方法,选择最快的方法。这可能会让你感到惊讶。

英文:

How many allocations are you intending to do? Two or three makes not the slightest difference. 10 million should be unnoticeable; if it isn't then your memory allocator is rubbish. One billion (probably with many deallocations as well), that's where you measure your code, see if it is a bottleneck, and try out different ways and pick what is fastest. Which might be surprising.

huangapple
  • 本文由 发表于 2023年7月28日 06:25:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/76783767.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定