Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Limit number of std::string allocations

I have a function that constructs a std::string from a const char* with two numbers, passed as parameters, appended to the end of it.

std::string makeName(const char* name, uint16_t num1, uint16_t num2) {

    std::string new_name(name);
    new_name.reserve(new_name.length()+5);

    new_name += ":";
    new_name += boost::lexical_cast<std::string>(num1);
    new_name += ":";
    new_name += boost::lexical_cast<std::string>(num2);

    return new_name;
}

This function gets called thousands of times to create unique names for small objects allocated on the heap.

Object* object1= new Object(makeName("Object", i, j)); // i and j are simply loop indices

I have discovered using valgrind's massif tool that the calls to makeName allocates a lot of memory since it gets called so many times.

87.96% (1,628,746,377B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.

->29.61% (548,226,178B) 0xAE383B7: std::string::_Rep::_S_create(unsigned long, unsigned long, std::allocator<char> const&) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)

| ->26.39% (488,635,166B) 0xAE38F79: std::string::_Rep::_M_clone(std::allocator<char> const&, unsigned long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)

| | ->26.39% (488,633,246B) 0xAE39012: std::string::reserve(unsigned long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)

| | | ->15.51% (287,292,096B) 0x119A80FD: makeName(char const*, unsigned short, unsigned short) (Object.cpp:110)

| | | | ->15.51% (287,292,096B) in 42 places, all below massif's threshold (01.00%)

My question is, how can I minimize these allocations to help reduce the overall total amount of memory my program uses?

EDIT: I also want to note that as a program requirement I cannot use c++11 features.

like image 593
burtmacklin16 Avatar asked Feb 05 '15 15:02

burtmacklin16


People also ask

How do you limit a string in C++?

There is no official limit on the size of a string. The software will ask your system for memory and, as long as it gets it, it will be able to add characters to your string.

Is there a size limit on STD string?

While an individual quoted string cannot be longer than 2048 bytes, a string literal of roughly 65535 bytes can be constructed by concatenating strings.

Does std :: string allocate?

However, std::string requires space to be dynamically allocated in the buffer and more memory to be dynamically allocated each time an operation is performed on the string.

Is std :: string contiguous?

A std::string's allocation is not guaranteed to be contiguous under the C++98/03 standard, but C++11 forces it to be. In practice, neither I nor Herb Sutter know of an implementation that does not use contiguous storage.


3 Answers

Only a DIY custom conversion beats using sprintf in such case.

So I would use sprintf and MEASURE.

Only if that wasn't good enough would I implement my own integer-to-string (knowing from numerous cases that it will certainly be somewhat faster, but not enough to justify starting with that).


Example. Instead of the current high level code

std::string makeName(const char* name, uint16_t num1, uint16_t num2) {

    std::string new_name(name);
    new_name.reserve(new_name.length()+5);

    new_name += ":";
    new_name += boost::lexical_cast<std::string>(num1);
    new_name += ":";
    new_name += boost::lexical_cast<std::string>(num2);

    return new_name;
}

just do

auto makeName( const char* const name, const uint16_t num1, const uint16_t num2 )
    -> std::string
{
    std::string result( strlen( name ) + 25, '\0' );    // 25 is large enough.
    int const n = sprintf( &result[0], "%s:%d:%d", name, num1, num2 );
    result.resize( n );
    return result;
}

Disclaimer: code not touched by compiler's hands.

like image 98
Cheers and hth. - Alf Avatar answered Oct 29 '22 06:10

Cheers and hth. - Alf


"My question is, how can I minimize these allocations"

It occurs to me that you have a reason for those names. Can you compute them at the time of need, rather than always generating the name in the constructor? That would be the best improvement - don't do it at all until needed.

If you happen to already have a virtual base, and the class type determines the string, that makes it really easy. Otherwise, an enumerated type could replace the string, and you have a lookup table.

Object* object1= new Object(i, j));
std::string Object::getName(){ compute here }

If this doesn't help, then you actually do need the string for each and every object, and you can only get a small speedup by optimizing that function. I noticed that you construct the string at one size then grow it afterwards. Also you could work with a char buffer then assign it to the member string (in the constructor).

like image 35
Kenny Ostrom Avatar answered Oct 29 '22 06:10

Kenny Ostrom


Yes, your code is doing a lot of allocations. Analyzing the allocations:

std::string new_name(name); // 1
new_name.reserve(new_name.length()+5); // 2

new_name += ":";
new_name += boost::lexical_cast<std::string>(num1); // possibly 4 (boost + operator+=)
new_name += ":"; // possibly 5
new_name += boost::lexical_cast<std::string>(num2); // possibly 7

'possibly' because it depends on the characters needed by the numbers (the higher, the more).

If you're really concerned about memory allocations, asprintf(not standard though) or your version (based on the return value of s(n)printf) is likely to be the best choice:

std::string makeName(const char* name, uint16_t num1, uint16_t num2)
{
     char *ptr = nullptr; 
     int size  = asprintf( &ptr, "%s:%u:%u", name, num1, num2);
  return std::string(ptr, size); // to avoid copying the chars
}  

Note: As @Cheersandhth.-Alf pointed out, in case std::string failed to allocate memory, ptr would be ptr is leaked. The best way to solve this would involve using std::unique_ptr but I'll leave you to work it out to your needs.

if you don't want to use asprintf, you can get a similar behavior with std::snprintf

std::string makeName(const char* name, uint16_t num1, uint16_t num2)
{
    int length = std::snprintf(nullptr, 0, "%s:%u:%u", name, num1, num2);

    if (length > 0 )
    {
        std::string new_name(length + 1, '\0');
        std::snprintf(&new_name[0], new_name.length(), "%s:%u:%u", name, num1, num2);

       return new_name;
    } 
    // else handle failure
} 

The difference with your version (I didn't use boost::lexical_cast but std::to_string) is very big: ran 500 times the first version uses 72,890 bytes while the second only 23,890! (measured with valgrind memcheck)

like image 44
edmz Avatar answered Oct 29 '22 07:10

edmz