Reading Notes: programming technology that improves C ++ Performance,
Chapter 1 tracking examples 1st concerns
The actual problem introduced in this chapter is: defining a simple Trace class to output the current function name to the log file. The Trace object has certain overhead, so the Trace function is not enabled by default. The problem is: how to design the Trace class to minimize the overhead introduced when the Trace function is not enabled.
1.2 use the status variable switch function
It is very easy to use macros to switch the Trace function. When the Trace function is not enabled, there is no overhead at all:
#ifdef TRACETrace trace("aaa");#endif
The disadvantage is that each switch needs to be re-compiled.
Using status variables has certain runtime overhead, but can ensure flexibility. It is a reasonable choice:
class Trace {public: ... static bool isTraceEnabled; void Debug() { if (isTraceEnabled) { ... } }}
1.3 delayed Creation
The Original Trace class has built-in string members, so that the constructor and constructor overhead are also required when Trace is not enabled. You can change it to string * and create the Member when it needs to be enabled.
If the Trace enabling time is much less than the total time, this method is very effective. Otherwise, the original method is better when the overhead of dynamic creation is greater than the fixed overhead of one constructor and destructor.
Chapter 2 constructor and destructor 2nd concerns
Inheritance and synthesis will cause the constructor and destructor overhead to exceed your expectation. The trade-off between code reusability and running performance is worth noting.
2.2 deredundant objects
Removing unnecessary object usage, whether it is a member variable in the class or a function parameter, will lead to unnecessary Construction and Analysis overhead.
2.3 de-redundant base class
Some base classes do not have member variables or provide interfaces. Such base classes are meaningless base classes, removing such a base class at the inheritance level can reduce the following Overhead:
2.4 balance between embedded objects and embedded pointers
If the object is directly embedded, the construction of object A will lead to the construction of object B and C, and the construction of object B1 B2 C1 C2 ...... A constructor will be formed, and the overhead will be huge when there are many layers.
If the embedded pointer is delayed, it is constructed only when used for the first time, but it brings new overhead. If it is frequently used, it is better to embed objects directly.
2.5 delayed Creation
Here, the definition and creation of objects are delayed as much as possible until all conditions are met before creation.
2.6 Remove redundant construction overhead
This mainly refers to unnecessary replication:
String s; s = "a"; // there is an extra overhead for assigning values. It is also possible to convert "a" to the constructor and destructor overhead of a string temporary object.
And initialization that is not in the initialization list:
Class A {public: A (const string & s) {name = s; // there is an additional overhead for assigning values} private: string name ;};
Chapter 2 virtual functions 3rd concerns
Advantages of virtual functions: dynamic binding and code saving. Avoid the overhead caused by virtual functions.
3.2 virtual function overhead
There are three types:
3.3 solution 1: do not inherit
If it is not inherited, the sub-classes are independent. The disadvantage is that the Code is flooded with a large number of switches, which are not flexible and can be ruled out.
3.4 solution 2: Inheritance
As described in section 3.2.1, member functions cannot be inline. Especially short and frequently used functions increase the overhead.
3.5 solution 3: Template
Use a template to implement the implicit interface:
template <typename LockType>void func(LockType &lock) { lock.Lock(); ... lock.Unlock();}
Implements an implicit interface that requires Lock and Unlock. Because the template is determined during compilation, the generated function can be inline, while saving the overhead of indirect jump of pointers.
The disadvantage is that compilation errors caused by templates are very difficult to debug. At the same time, C ++ does not support this implicit interface, and the template interface requirements are often wrong during development.
Chapter 2 Return Value optimization 4th considerations
At any time, as long as the object is created and cleared, the performance gains will be obtained. The compiler removes the creation and cleanup of temporary objects whenever possible. This optimization is called the return value optimization (RVO ).
4.2 The compiler can perform RVO on anonymous objects
An anonymous object is directly returned at the end of the function. RVO can often be performed:
string Func() { string a = "a"; return a;}string FuncRVO() { return string("a");}
FuncRVO is easier to implement RVO than Func. the compiler removes the returned object and assigns its value directly to the object that receives the returned value.
4.3 active RVO
If Class A has the "+" Operation:
const A operator +(const A &x, const A &y) { A z(x); z += y; return z;}
Then add A constructor in:
class A {public: A(const A &x, const A &y); // A = x + y;};
Then, change "+" to the following form to get RVO benefits:
| 123 |
const A operator +(const A &x, const A &y) { return A(x, y);} |
The advantage is that it reduces the construction and analysis structure of a temporary object, and adds a similar constructor for all operations that require RVO, which is too flexible. If you have high performance requirements, you can consider this optimization method.
Chapter 2 temporary objects 5th concerns
How to avoid unnecessary temporary objects.
5.2 Type Mismatch
Assignment between different types can easily lead to the creation of temporary objects. This implicit conversion can be avoided by adding explicit it before a single-parameter constructor.
5.3 avoid repeated creation of the same temporary object
In the following loop:
Complex a;for (int i = 0; i < 10; ++i) { a += 1.0;}
In each loop, a Complex object with a value of 1.0 is created. You can create a Complex object with a value of 1.0 outside the loop to reduce this overhead:
Complex one(1.0);for (int i = 0; i < 10; ++i) { a += one;}
Chapter 2 single-thread memory pool 6th concerns
The performance of the default general memory manager may cause certain performance bottlenecks in specific scenarios. This chapter discusses how to implement a memory pool manager with better performance than general new/delete in a single-threaded environment when a fixed-size or unfixed-size memory is allocated each time.
6.2 test code
Rational *array[1000];for (int j = 0; j < 500; ++j) { for (int i = 0; i < 1000; ++i) { array[i] = new Rational(i); } for (int i = 0; i < 1000; ++i) { delete array[i]; }}
6.3 Rational dedicated memory pool
Each time a Rational memory block is allocated, an idle linked list is used to maintain the allocated idle memory. When the memory block is released, it is re-stored in the linked list:
class Rational { ... static list<char *> freeList; void *operator new(size_t size) { if (freeList.empty()) { return new char[sizeof(Rational)]; } else { void *buf = freeList.back(); freeList.pop_back(); return buf; } } void operator delete(void *ptr, size_t size) { freeList.push_back(ptr); }};
The memory pool of this version never shrinks. To release the memory, you need to add an interface.
Compared with the general memory manager, the memory pool of this version has the following benefits:
6.4 fixed size memory pool
Not only for Rational, but also for classes of any fixed size:
template <typename T>class FixedSizeMemoryPool {public: FixedSizeMemoryPool(): size_(sizeof(T)) {} ~FixedSizeMemoryPool() { for(char *&p: freeList_) { delete[] p; } } void *Alloc() { if (freeList_.empty()) { return new char[size_]; } else { void *buf = freeList_.back(); freeList_.pop_back(); return buf; } } void Free(void *buf) { freeList_.push_back(buf); }private: list<char *> freeList_; const size_t size_;};
For Rational, you need to change it:
class Rational {public: void *operator new(size_t size) { return pool.Alloc(); } void operator delete(void *ptr, size_t size) { pool.Free(ptr); }private: static FixedSizeMemoryPool<Rational> pool;};
6.5 unlimited memory pool
The management method for memory pools of an indefinite size is different from the previous version, because there is no way to directly return a memory block from the linked list (the size is different ). Here, we allocate a large fixed-size memory block as needed. Each time we allocate the memory of a single object, we will allocate it from this memory block. When there is not enough space, we will allocate a large memory block.
With the increase of universality, the performance is also decreasing. Therefore, the sacrifice of flexibility and versatility may have a good effect when performance is very required.
Chapter 2 multi-thread memory pool 7th concerns
A mutex lock is added to the memory pool of a single thread to implement a working distributor in a multi-threaded environment. You can pass in the lock parameters when initializing the distributor.
7.2 pthread_mutex Memory Pool
Add a new template parameter typename LockType to the MutableSizeMemoryPool in the previous chapter, which allows the input of a LockType * and the lock is applied when Alloc and Free are used. The other parameters remain unchanged.
The performance of the distributor in this version is not good, because the performance of pthread_mutex exceeds our needs, and we may only need a simple lock for the function. If you can pass in a mutex lock of a more primitive version, it will have better performance.
7.3 increase the scalability of the Memory Pool
The current version of the memory pool has poor performance in highly concurrent environments, because access to memory blocks (Alloc and Free) must be serialized. You can add a list of multiple memory blocks and lock each list separately. This way, you can distribute multiple requests to different lists for processing at the same time.
Chapter 2 inline basics 8th considerations
Inline may improve performance, but may also reduce performance. This chapter focuses on how to avoid negative effects and make good use of positive benefits.
8.2 benefits: Remove function calls
Removes the overhead of function calls. Common function calls include:
In addition, the loss of CPU idling caused by redirection is also avoided.
8.3 benefits: Cross-Function Optimization
This allows the compiler to optimize variables across functions:
int Inc(int x) { return x + 1;}void Func() { int y = Inc(1);}
Func is equivalent:
void Func() { int x = 1; int y = x + 1;}
The compiler can even be further optimized:
void Func() { int y = 2;}
Through inline, the compiler can rearrange a large number of methods, omitting a large number of unnecessary statements, and even including object creation and clearing.
8.4 benefits: shorten the Key Path
Inline functions on the Key Path can shorten the key path without changing the program logic, thus greatly improving the performance.
8.5 loss: size after compilation
After inline, the function code is displayed at each call point. If the number of function code and call points is large, the code size will expand. On the one hand, it will slow down code loading, and on the other hand, it will lead to more frequent page defects.
However, if the function length is particularly short and shorter than the entire call process, the inline function will reduce the code size, which must be inline.
8.6 loss: the Code must be re-compiled after modification
If an inline function modifies the code, it must be re-compiled where it is used, because the code of this function has been expanded in various call points. Therefore, large projects are often inline at the end.
Chapter 2 inline-performance considerations 9th concerns
This chapter focuses on the 2nd benefits of inline: After the code of inline functions is expanded, the compiler optimizes the performance of these functions.
9.2 inter-call Optimization
After inline, the code for calling a function is mixed with the code for calling, which allows the compiler to perform many advanced optimizations, just like reorganizing the code. Especially for the optimization of direct traffic:
int Choice(int x) { switch (x) { case 1: return 5; break; case 2: ... ... case 100: return 301; break; default: return 0; break; }}int x = Choice(100);
After inline optimization, the above Code may only be left:
int x = 301;
9.3 why not use inline
The main disadvantage of inline is that it may increase the size of the Code. Especially when a relatively large method is embedded in multiple layers, the volume index expands.
Disadvantage 2: all modifications need to be re-compiled.
Disadvantage 3 is that it is difficult to debug inline functions because the actual function is no longer available and the function Entry and Exit cannot be traced.
9.4 configuration-based inline
The size, number of calls, call points, and other information of each function after compilation should be counted before the internal connection. The configuration information is used to determine which functions are inline.
Function dimensions can be divided:
For functions with smaller static sizes and larger dynamic sizes, inline operations will greatly benefit. For functions with only one call point, such as calls within a loop, inline is almost always correct. For functions with many call points and calls, it is best to rewrite them to show their fast paths and then inline them.
A function is as follows:
void FuncX { if (/* error handle code */) { ... // 30 lines } ... // real work (5 lines)}
FuncX has about 40 lines of code, which is not suitable for inline on the surface, but if you split its error handling code into a separate function:
void FuncX { if (...) FuncY(); ... // real work (5 lines)}void FuncY { ... // error handle code (30 lines)}
At this time, FuncX only has 7 lines of code, which is suitable for inline. This is also equivalent to splitting code segments with Large static sizes and small dynamic sizes, so that the remaining code with small static sizes and Large Dynamic sizes can be inline.
9.5 suitable for inline functions chapter 1 inline skills 10th considerations
Some tips that can help you better inline.
10.2 condition inline
If you want to use a pre-compilation option to control when to inline certain functions and when to disable inline, you can use the conditional inline technique.
Put the inline function definition in. inl, put the definition of other functions in. cpp, and then add the following in. h:
#ifdef INLINE#include "*.inl"#endif
Add:
#ifndef INLINE#define inline // let inline be void#endifinline FuncX(...){}
Add:
#ifndef INLINE#include "*.inl"#endif
10.3 selective inline
You can inline a function at some call points, but not at other call points. I am not very fond of the specific content. I skipped it.
10.4 recursive inline
The tail recursion function can be changed to the iteration function, and then the inline method can be searched.
If a non-tail recursive function is very performance-oriented, you can expand the function to a certain extent:
void RecursiveInline() { ... Recursive(); ...}void Recursive() { ... RecursiveInline(); ...}
Inline the previous function, which will speed up the operation, but will also significantly increase the size after compilation.
It can also be expanded manually or maintained using a macro, but it is easy to cause problems.
10.5 special architecture
In some architectures (such as Linux), the overhead of function calling is very low when there are few calling layers. In this case, the benefits of inline functions that are not tiny are not obvious. Therefore, any internal connection to non-tiny functions must be based on the configuration information.
Chapter 1 Standard Template Library 11th focus 11.1 better than STL
If it is better than STL, it is often necessary to sacrifice a certain degree of versatility and flexibility to start optimization from some specific environmental factors.
Chapter 2 reference count 12th concerns
C ++ uses reference counting to solve the garbage collection problem. The basic idea is to transfer the responsibility for clearing objects from the client code to the object itself.
The reference count can reduce memory usage and avoid Memory leakage, but it may have disadvantages in terms of execution speed, especially in multi-threaded environments.
12.2 Implementation of reference count
Implementation A: Class built-in reference count. Class RefCountBase encapsulates the operations related to the reference count, and the class that needs to implement the reference count inherits it:
class RefCountBase {public: Attach() { ++refCount_; } Detach() { if (--refCount_ == 0) { delete this; } }protected: RefCountBase(): refCount_(0) {} RefCountBase(const RefCountBase &rc): refCount_(0) {} RefCountBase &operator=(const RefCountBase &rc) { return *this; } virtual ~RefCountBase() {} size_t refCount_;};
For example, Class A inherits from RefCountBase. To implement reference counting, A proxy class SmartPtr is also required to act as A smart pointer:
template <typename T>class SmartPtr {public: SmartPtr(T *ptr = nullptr): ptr_(ptr) {} SmartPtr(const SmartPtr &sptr): ptr_(sptr.ptr_) { if (ptr_) { ptr_->Attach(); } } SmartPtr &operator=(const SmartPtr &sptr) { if (sptr.ptr_) { sptr.ptr_->Attach(); } if (ptr_) ptr_->Detach(); ptr_ = sptr.ptr_; return *this; } T *operator->() { return ptr_; } T &operator*() ( return *ptr_; )private: T *ptr_;};
Implement B: add the count function to SmartPtr. Remove RefCountBase, but add a size_t * count _ in SmartPtr. The Attach operation on ptr _ is changed to ++ * count _, And the Detach operation is changed to -- * count _. Others are the same.
12.3 concurrent reference count
In SmartPtr, you must perform operations on count _ and ptr _ at the same time. In a concurrent environment, this means that you need to lock before and after the operation to ensure atomic operations on the two objects.
12.4 performance of reference count
To implement A, you must modify the original class. If you cannot perform such modification, you can only use B. In implementation B, because you need to operate the members (count _ and ptr _) on the two stacks, the creation and clearing performance will be worse than the Implementation of.
The benefit of reference counting is:
Disadvantages of reference count:
The following conditions increase the benefit of reference count:
Chapter 2 code optimization 13th considerations
Many performance problems will be introduced in the application coding stage. These problems are usually small-scale problems. To solve them, you do not need to look at too much code or change the in-depth design, however, it may lead to significant performance improvement.
13.2 Cache
Remember the results of frequent computation and high computing costs. Typically, the fixed results that need to be calculated repeatedly in the loop are stored in a variable outside the loop and used in the loop.
13.3 pre-Calculation
Some computing results that need to be frequently used in key paths can be computed in advance, and the results are saved. In this way, you only need to perform a simple search.
13.4 reduced flexibility
If the scope of the target code is fixed, you do not need to consider too many general situations in the Code. Instead, you can make bold assumptions about some specific situations that are currently known to speed up running.
13.5 increase the speed of common paths.
80% of the time is spent on 20% of function calls, so the time required to reduce the 20% function can greatly improve the performance of the entire system.
Similar examples appear in if (and1 & and2) And if (or1 | or2). if the two conditions have no dependency, in this case, the conditions that are more likely to determine the entire Relational Value are put in front, that is, if and1 is more likely to be false than and2, then and1 is put in front, if or1 is more likely to be false than or2, or2 will be created first.
In addition to the possibility of a condition value, you can also take into account the number of instructions for each operation, so that the minimum number of conditions can be placed in front.
If 5% of all external parameters are special and the other 95% is similar, we can design a path for these 5% special parameters separately, this accelerates the processing of 95% of common cases.
13.6 slow computing
Delay computing to what is actually needed, so that expensive computing results are not used at the end. There is nothing new in this section.
13.7 useless computing
There is nothing new in this section.
13.8 Architecture
The influence of the architecture is taken into account when designing the object layout, mainly because of the impact of the system cache. For example, if the row length of the matrix is exactly the same as that of the cache row, frequent cache miss occurs when the matrix is transposed. When designing two members that often need to be accessed together, it is best to make them in the same cache row. This also requires the first accessed members to be put in front.
13.9 Memory Management
Performance is a transaction. Nothing new.
13.10 database and system calls
Many performance details are hidden behind libraries and system calls. Therefore, you need to have a detailed understanding of these details during design and select the one with just enough functions among multiple available tools, its performance is often better than those with more comprehensive and powerful functions.
13.11 Compiler Optimization
Enabling Compiler Optimization in release may greatly improve the performance.
Chapter 1 design optimization 14th considerations
The design optimization is global and relies on other components and code.
14.2 design flexibility
In the early stages of software development, if you do not know the hot spots of the program, you can fully use STL. When you have a certain understanding of program running, you can use some flexibility in exchange for performance.
14.3 cache: Timestamp
Each request in the web Service needs to write logs with a timestamp. If a single request requires multiple writes, the computed Timestamp can be cached for all these logs.
14.4 cache: Data Expansion
If the object often needs to return the value of an operation, you can embed the value in the object, such as the size of various containers.
14.5 cache: Public Code traps
If a piece of code needs to judge the request type each time to determine the running path, you can split it into two sections of code for a single operation. This is a field that virtual functions are very good.
14.6 efficient data structure
Nothing new.
14.7 slow computing, useless computing, and invalid code
Nothing new.
Chapter 1 scalability 15th concerns
Performance issues in parallel or concurrent environments.
15.2 SMP system
One performance bottleneck of the SMP system is that multiple processors need to share the bus with the memory.
The solution is to configure a large cache for each processor, but the main problem is cache consistency.
The preceding two problems make it difficult to increase the parallel performance to a multiple of the increase in the number of cores.
15.3 Amdahl's Law
Sequential computing is a major obstacle to scalability. The increase caused by a single acceleration segment will not be greater than the total overhead of the segment.
15.4 split the task
Splitting a single task into multiple concurrent subtasks can improve the following indicators:
I/O-intensive tasks are more suitable for concurrent execution.
15.5 cache shared data
Example: A thread serves a request. during the lifetime of the thread, many operations of the thread must act on the request. One idea is to call pthread_getspecific in each operation, but this will cause serious lock overhead. Another idea is to get a pointer at the beginning of the thread and pass it to all subsequent functions.
15.6 No sharing
In the above example, a better way is to directly put thread-related items in the structure associated with the thread, so that the serialized part can be completely removed.
Part 1 sharing
When you do not know the number of requests, you can use a fixed size thread pool for service, which has good scalability.
15.8 lock Granularity
Generally, it is not a good idea to integrate multiple irrelevant resources into the protection of a single lock. The exception is when the following conditions are met:
The granularity of the lock is too coarse, leading to a reduction in parallelism, And the granularity is too small, leading to an increase in the lock overhead and a growth in the processing time of a single task.
15.9 pseudo-sharing
On the SMP system, if two locks are in the same cache row, the p1 lock on m1 will cause the entire cache row to become invalid on p2, as a result, p2 needs to re-read the memory when accessing m2. To avoid this problem, manually insert a blank space between m2 and m2.
15.10 Starbird
If multiple threads are used for accept, such as 100, 100 threads will wake up when connecting requests, but only one thread can get the request, and the other 99 threads will continue to sleep. This CPU impact will cause the server to shrink and seriously damage the throughput. When throughput drops, the system may increase more threads, resulting in more serious problems.
The solution is to use only one accept thread to distribute requests to other threads.
15.11 read/write lock
Nothing new.
Chapter 1 system architecture relevance 16th concerns
The content discussed in this chapter is as follows: