1

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?

user207421
  • 305,947
  • 44
  • 307
  • 483
Ashkanxy
  • 2,380
  • 2
  • 6
  • 17
  • 6
    Try benchmarking it. – HolyBlackCat Jul 27 '23 at 22:29
  • `auto num_fillers = inp.length() - 4;` looks risky. Is `inp` always at least 4 characters long? If not, you may end up with the result being `std::numeric_limits::max()` . – Ted Lyngmo Jul 27 '23 at 22:29
  • What type is `expect_len`? What is `res(...)` supposed to do? `std::string` does not have a function call operator. Why use `num_fillers + expect_len` instead of `inp.length()`? How do you expect us to compare pieces of code that do not compile and that would not give the same result anyway if they did? – Nelfeal Jul 27 '23 at 22:34
  • 1
    @Ted Lyngmo thanks for your note. Yes assume it has always length of larger or equal to 4 characters, but my main question is about generating the result string – Ashkanxy Jul 27 '23 at 22:35
  • 1
    Ok, then just benchmark it as @HolyBlackCat suggested. Here's a nice site making it easy: https://quick-bench.com/ – Ted Lyngmo Jul 27 '23 at 22:37
  • Are you programming on an embedded system? Do you have virtual memory? At some point on an embedded system, the allocations will lead to a fragmented memory. – Thomas Matthews Jul 27 '23 at 22:43
  • 1
    Go through you system and find out which allocations are actually needed. Some people program C++ like Java and use `new` for every variable declaration. – Thomas Matthews Jul 27 '23 at 22:45
  • 1
    Use the benchmarking tool Ted pointed you at. Benchmarking is HARD, and a lot of people make subtle mistakes that lead to them choosing the wrong implementation. – user4581301 Jul 27 '23 at 22:49
  • In that PARTICULAR example, there is no benefit, because the "+" operator would do exactly what your code is doing. – Tim Roberts Jul 27 '23 at 22:52
  • This is not within the province of [tag:compiler-optimization] in any way shape or form. Don't tag indiscriminately. – user207421 Jul 27 '23 at 23:14

4 Answers4

5

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);
paddy
  • 60,864
  • 6
  • 61
  • 103
  • 1
    You should consider to change the parameter type to `std::string_view`, otherwise calls like `method_C("Too long for SBO")` will incur one extra useless allocation. – chrysante Jul 27 '23 at 23:19
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)

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • Fine general advice but how does that relate to OP's code? – Nelfeal Jul 27 '23 at 23:03
  • 1
    In `method_B()` the OP precalculates the maximum string length and passes it to `reserve()` so that the string only needs to allocate its internal buffer once. In `method_A()`, OTOH, he just manipulates the strings as necessary, without worrying about minimizing the number of internal buffer reallocations. My answer discusses the tradeoffs between those two approaches. – Jeremy Friesner Jul 27 '23 at 23:09
  • I don't see that in your answer. You are just saying that there is generally a tradeoff between efficiency and readability. However, I don't find either of OP's functions to be particularly more readable or efficient than the other. OP seems to believe that creating more local (and temporary?) strings leads to worse performance, but that is incredibly far from the whole story. – Nelfeal Jul 27 '23 at 23:39
  • The question at hand is, is it worth precomputing a string's final size to avoid heap re-allocations during the string's construction? If maximizing runtime-efficiency is more important than future-proofing the code, then maybe it is, but that's probably more the exception than the rule. – Jeremy Friesner Jul 27 '23 at 23:52
3

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.

Nelfeal
  • 12,593
  • 1
  • 20
  • 39
  • This is the best answer. I was going to update mine to show an in-place operation but you beat me to it! :) This method not only avoids the possibility of redundant allocations, but is much easier to read than all the alternatives. – paddy Jul 27 '23 at 23:07
-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.

gnasher729
  • 51,477
  • 5
  • 75
  • 98
  • 3
    Not a useful answer, and not an accurate one either. You are making claims with no relations to OP's code. Also, 10 million allocations can absolutely be noticeable, depending on how big they are. – Nelfeal Jul 27 '23 at 22:39
  • 10 million allocations every how often? For every line of input you process? Every second? Every millisecond? – Peter Cordes Jul 27 '23 at 23:34