Need For Speed: part 2
Reduce heap allocations
This talk is not about:
-
Reduce the amount of RAM your application requires
-
Special allocators (memory pools, arena allocators,...)
CppCon 2017: John Lakos
“Local ('Arena') Memory Allocators"
Problem
Some C++ features are SO convenient that the user sometimes forget about the cost of heap allocations
C style manual memory management is NOT the solution
Measure first!
- Valgrind / Callgrind
- Perf ( Hotspot )
- Visual Studio
- Intel VTune
Only if you saw that new and delete
take a relevant amount of CPU, try to reduce heap allocations
Tip #1: use Small Object Optimizations
std::string can be implemented to avoid allocations for strings with size <= 23 characters
There are implementations of std::funtion which use stack memory only (no heap allocations)
Many implementations of SmallVector<T, SIZE>, which do not allocate on the heap if size() < SIZE
Prefer std::array<T,Size> over std::vector<T>
class String
{
public:
String(const String& s) { ... }
String(const char* s) { ... }
// all the methods of std::string
private:
union Data {
struct NonSSO {
char* ptr; // 8 bytes
std::size_t size; // 8 bytes
std::size_t capacity; // 8 bytes
} non_sso;
struct SSO {
char string[ 23 ]; // 23 bytes
uint8_t size; // 1 byte
} sso;
} m_data;
};
https://github.com/elliotgoodrich/SSO-23
CppCon 2016: Nicholas Ormrod
“The strange details of std::string at Facebook"
Tip #2: use std::move
- Move resources from one instance to another.
- Usually it invalidates the source object.
- Prevent most of the cost of copying and heap allocations
- Particularly useful to insert into a STL containers (and more intuitive than emplace)
void StringPush()
{
std::vector<std::string> buffer;
buffer.reserve(100);
for(int i=0; i<100; i++)
{
std::string created_string("long string that requires memory allocation");
buffer.push_back(created_string);
}
}
void StringMove()
{
std::vector<std::string> buffer;
buffer.reserve(100);
for(int i=0; i<100; i++)
{
std::string created_string("long string that requires memory allocation");
buffer.push_back( std::move(created_string)) ;
}
}
(real world example)
Recycling in action
struct Point {
float x, y, z;
float red, green, blue;
std::vector<float> features; // this usually contains 3 to 8 elements
};
struct PointCloud {
std::vector<Point> points; // usually 10.000 +- 40% points.
std::map<std::string, float> features;
};
std::deque<PointCloud> buffer;
// This function alone takes 38% of the CPU
void DeserializePointCloud(const PointCloudMessage& msg)
{
PointCloud cloud;
while( buffer.size() >= MAX_BUFFER_SIZE) {
buffer.pop_front();
}
cloud.features = // deserialize from message
for (int i=0; i < N_POINTS; i++)
{
Point point = // deserialize from message
cloud.points.push_back( point );
}
buffer.push_back( cloud );
}
We can do better!
// This function is now 2X faster than the previous one
void DeserializePointCloud(const PointCloudMessage& msg)
{
// recycle an instance already created instance of
PointCloud cloud;
if( buffer.size() >= MAX_BUFFER_SIZE) {
cloud = std::move( buffer.front() );
}
while( buffer.size() >= MAX_BUFFER_SIZE) {
scan_buffer.pop_front();
}
cloud.features = // deserialize from message
cloud.points.resize( N_POINTS );
for (int i=0; i < N_POINTS; i++)
{
cloud.points[i] = // deserialize from message
}
buffer.push_back( std::move(cloud) );
}
Recycle an already allocate instance
Resize, don't reserve
Move, do not copy
Tip #3: think about your API
std::string IntToString( int num );
int StringToInt( std::string text );
std::string IntToString( int num );
int StringToInt( const std::string& text );
bool IntToString( int num, std::string& output);
bool StringToInt( const std::string& text, int& number );
Better...
Best... (?)
void TakesCharStar(const char* s); // C convention
void TakesString(const std::string& s); // C++ convention
//----------------------------------------------------
// your code
const char* char_array = "helloWorld";
std::string cpp_string("helloWorld");
TakesCharStar( char_array ); // OK
TakesCharStar( cpp_string.c_str() ); // ugly ?
TakesString( char_array ); // potentially allocate memory
TakesString( cpp_string ); // OK
void TakesStringView( std::string_view s);
//----------------------------------------------------
// your code
const char* char_array = "helloWorld";
std::string cpp_string("helloWorld");
TakesStringView( char_array ); // OK
TakesStringView( cpp_string ); // OK
Exceptions to tip #3:
Return Value Optimization
Sometime you do not have to worry, because the compiler calls std::move for you under the hood
Let's play a game...
struct Noisy
{
Noisy() { std::cout << "constructed\n"; }
Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
~Noisy() { std::cout << "destructed\n"; }
};
std::vector<Noisy> f()
{
std::vector<Noisy> v = std::vector<Noisy>(2);
return v;
}
void PrintSize(std::vector<Noisy> arg)
{
std::cout << "arg.size() = " << arg.size() << '\n';
}
int main()
{
std::vector<Noisy> v = f();
PrintSize(v);
return 0;
}
constructed
constructed
copy-constructed
copy-constructed
arg.size() = 2
destructed
destructed
destructed
destructed
Guess the output...
Guess the output...
std::vector<Noisy> f()
{
std::vector<Noisy> v = std::vector<Noisy>(2);
return v;
}
int main()
{
std::vector<Noisy> v = f();
return 0;
}
constructed constructed destructed destructed
std::vector<Noisy> f()
{
std::vector<Noisy> v = std::vector<Noisy>(2);
return v;
}
void PrintSize(std::vector<Noisy> arg)
{
std::cout << "arg.size() = " << arg.size() << '\n';
}
int main()
{
std::vector<Noisy> v = f();
return 0;
}
constructed constructed
arg.size() = 2
destructed destructed
Guess the output...
Reduce heap allocations
By Davide Faconti
Reduce heap allocations
- 598