Performance Bottleneck from Hidden Atomic Operations A benchmark of multi-threaded C++ programs shows that copying the same std::shared_ptr once per iteration forces every thread to contend on a single cache line, because each copy increments and decrements the shared control block's atomic reference count. The comparison program, which instead uses a reference to an immutable object kept alive outside the timed region, performs the same lightweight work without that contention, so throughput scales with thread count where the shared_ptr version does not. The benchmark is built with g++ -O3 -DNDEBUG -std=c++17 -pthread and run with --max-threads 16 --iterations 20000000. Performance Bottleneck from Hidden Atomic Operations Atomic operations are very useful for ensuring correct behavior in concurrent programs, while sacrificing much less performance compared to traditional locking mechanisms. However, for high performance C, C++, and CUDA concurrent programs, experienced developers are aware that even atomic operations can become a performance bottleneck, and they will try to minimize their usage whenever possible. However, some atomic operations are hidden behind seemingly innocuous constructs, such as copying a shared pointer, which can lead to unexpected performance degradation. In this blog post, we will explore a common performance bottleneck caused by hidden atomic operations, specifically focusing on the case of copying shared pointers in multi-threaded C++ programs. We will demonstrate how seemingly harmless operations can introduce contention and degrade performance, and discuss strategies to mitigate these issues. In the following code, we will set up a benchmark to measure the throughput scaling with the number of threads. In one implementation, each thread will repeatedly copy the same shared pointer followed by some extremely lightweight work on the data it points to. In the other implementation, each thread will use a reference to an immutable object followed by the same extremely lightweight work on the referenced object. These two implementations look similar and interchangable at first glance. In practice, seeing the first implementation is also common. If there is no performance difference between using shared pointers and using references to immutable objects, the throughput should scale with the number of threads and both implementations should exhibit similar performance scaling. | 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 | include