Slab's "Object reuse"
So far, the Slab allocator invented by Sun in 1991 is regarded as the best overall performance among various OS kernel memory allocator. It has several measures to improve the Memory Allocation performance, one of which is "Object reuse ".
Principle
OS can use slab to provide application and release of general memory blocks. The so-called general memory blocks refer to memory blocks that can be used for non-specific purposes. After they are applied, you can build an object or use it as a buffer. In this case, each slab cache is a pool that provides a fixed size of general memory blocks. In Linux 2.4, there are 13 slab caches, which are used to provide general memory blocks of 32, 64, 128,256,... and 131072 respectively.
In addition to such slab cache, OS can use slab to provide Allocator for specific objects. In an slab cache that provides specific objects, only one type of objects can be allocated. Different specific objects are allocated by their own slab cache. For example, inode and vm_area have their own slab cache.
For a specific type of slab cache, you can use the "Object reuse" technology.
In fact, the performance improvement of slab's "Object reuse" is not purely an improvement in the memory allocation and release operational performance. We use the following method to build objects from a memory distributor using C ++:
Type_pointer * _ p_object = new type_pointer (_ init_para1, _ init_para2 );
This statement contains two processes:
1) allocate memory for building objects from Memory Allocator;
2) Call the constructor function of class type_pointer to construct the object.
The method for destroying C ++ objects is as follows:
Delete _ p_object;
This statement also contains two processes:
1) Call the destructor function of class type_pointer;
2) release the memory occupied by the object back to memory allocator.
Generally, memory allocator is only responsible for memory release and application, that is, process 1 in the above object construction process and process 2 in the object destruction process. However, slab can write an article in process 2 of Object Construction and process 1 of object destruction.
In the traditional way, every object's lifecycle always includes the following five processes:
1) allocate memory for building objects from Memory Allocator;
2) Call the constructor function of the class;
3) use it;
4) Call the destructor function of the class;
5) release the memory occupied by the object back to memory allocator.
Because slab is well designed for memory allocation and release, in most cases, memory allocation is very fast, and in all cases, memory release operations are very fast. In comparison, the constructor and destructor processes of objects take most of the time for object construction and destruction, especially for complex objects.
Take a closer look at the five processes listed above, and then let your brain cells perform some movements, you will find an important fact: Since the slab cache of a specific object stores the same object, if the user of the object can ensure that the object obtained by the object is restored to the state after the object has just called the constructor function before it is released, then the object does not need to be called the destructor function; instead, we only need to put this object back into slab cache. The next time the memory is re-allocated, the State is the same as that after the constructor is called, therefore, you no longer need to call constructor again. Until finally, when the slab cache block is returned to the lower-layer Memory Manager, that is, when the large memory size of this object does not belong to the current slab cache, call its destructor.
Therefore, for the slab cache of a specific object, all the object memory blocks must be called constructor once in multiple allocation and release operations, you only need to call destructor once. After these time-consuming redundancy processes are cleared, the performance can naturally be significantly improved.
When we are excited for this major improvement, we must keep in mind the premise included in this improvement and repeat it again-"The caller must ensure that after use, restore the object to the state before use, that is, the State after the object has just been called constructor ".
Example
In the early days, slab was not used in Linux. However, slab's reputation is booming, so that Linux developers can't afford it anymore.
The slab cache Creation Interface for Linux 2.4 is:
Kmem_cache_t * kmem_cache_create (const char * _ name, size_t _ size,
Size_t _ offset, unsigned long _ flags,
Void (* _ ctor) (void *, kmem_cache_t *, unsigned long ),
Void (* _ dtor) (void *, kmem_cache_t *, unsigned long ));
The last two parameters of this function, __ctor and _ dtor, are the constructor and destructor functions of the current slab cache object.
Next, let's look at an example --
Suppose we have an object whose type is defined:
Struct object {
Mutex_t lock;
Char * buffer;
Int count;
};
Its constructor is:
Void object_ctor (void * _ MEM, kmem_cache_t *, unsigned long)
{
Struct object * _ OBJ = (struct object *) _ MEM;
Mutex_init (& __ obj-> lock );
_ Obj-> buffer = NULL;
_ Obj-> COUNT = 0;
}
Its destructor is:
Void object_dtor (void * _ MEM, kmem_cache_t *, unsigned long)
{
Struct object * _ OBJ = (struct object *) _ MEM;
Assert (_ obj-> buffer = NULL );
Assert (_ obj-> COUNT = 0 );
Assert (! Mutex_test_lock (& __ obj-> lock ));
Mutex_destory (& __ obj-> lock );
}
In this example, after a struct object is called object_ctor, its status is:
1) the lock is created and initialized, and is in the unlock status;
2) The buffer pointer is null;
3) The value of count is 0.
Then, it can use this object, such as the following code:
Void foo_change (Object * _ OBJ, size_t _ count)
{
Mutex_lock (& __ obj-> lock );
_ Obj-> buffer = (char *) malloc (_ count + 1 );
_ Obj-> COUNT = _ count;
}
This code changes the status of _ OBJ because its buffer and count fields have changed. Therefore, you must restore the status of _ OBJ before returning it to slab.
Void foo_recovery (Object * _ OBJ)
{
If (_ obj-> buffer)
Free (_ obj-> buffer );
_ Obj-> COUNT = 0;
If (mutex_test_lock (& __ obj-> lock ))
Mutex_unlock (& __ obj-> lock );
}
This example shows how to use slab's "Object reuse" mechanism. It should be enough. But I still don't want my brain to stop thinking about this issue so quickly. Let's move on to the next step --
The above example is written in C language. We may wish to change it to the implementation of C ++.
Class Object
{
Mutex_t lock;
Char * buffer;
Int count;
Public:
Object (void)
{
Mutex_init (& lock );
Buffer = NULL;
Count = 0;
}
~ Object ()
{
Assert (buffer = NULL );
Assert (COUNT = 0 );
Assert (! Mutex_test_lock (& lock ));
Mutex_destory (& lock );
}
Void foo_change (size_t _ count)
{
Mutex_lock (& lock );
Buffer = (char *) malloc (_ count + 1 );
Count = _ count;
}
Void foo_recovery (void)
{
If (buffer)
Free (buffer );
Count = 0;
If (mutex_test_lock (& lock ))
Mutex_unlock (& lock );
}
};
For C ++, because of its own ctor/dtor mechanism, when we use new to allocate an object from Slab allocator, the construct of the class will be automatically called. Similarly, when we use Delete to destroy an object, the Destructor is also called automatically. This is the mechanism of the language itself and we cannot avoid it. In this way, the constructor function and destructor function of the class are called every time the object is constructed/destroyed. In this case, the "Object reuse" provided by slab is useless.
Is it difficult for a language to affect the use of such an important function? What's more, C ++ is also a general programming language !! I think you are beginning to feel uneasy, angry, and finally in despair.
Before you despair, take a few deep breaths, observe the situation carefully, and seriously consider countermeasures. It is very likely that "Liu an hua ming ".
After your mood is stable, let's take a look at the C language example above.
In this example, the object contains two resources:
1) Resource lock constructed by object_ctor;
2) Resource buffer constructed by the caller; count should also be calculated.
God exists, and it teaches us in the Bible: "dust returns to dust, Earth returns to Earth ". So we should let slab do what it should do -- construct and destroy the lock; let the class ctor/dtor construct and destroy the buffer. Therefore, the above C ++ rewrite is incorrect, and the correct implementation should be:
Class Object
{
Public:
Mutex_t lock;
Char * buffer;
Int count;
Public:
Void object (size_t _ count)
{
Mutex_lock (& lock );
Buffer = (char *) malloc (_ count + 1 );
Count = _ count;
Mutex_unlock (& lock );
}
Void ~ Object ()
{
If (buffer)
Free (buffer );
Count = 0;
If (mutex_test_lock (& lock ))
Mutex_unlock (& lock );
}
Public:
Static void ctor (void * _ OBJ, kmem_cache_t *, unsigned long)
{
Object * _ OBJECT = static_cast (_ OBJ );
Mutex_init (& __ object-> lock );
_ Object-> buffer = NULL;
Object-> COUNT = 0;
}
Static void dtor (void * _ OBJ, kmem_cache_t *, unsigned long)
{
Object * _ OBJECT = static_cast (_ OBJ );
Assert (_ object-> buffer = NULL );
Assert (_ object-> COUNT = 0 );
Assert (! Mutex_test_lock (& __ object-> lock ));
Mutex_destory (& __ object-> lock );
}
};
Function ctor and dtor are the constructor and destructor that slab calls. It is very important that they must be static or simply implement them as external processes.
Then, we can create the slab cache as follows:
Typedef void (* slab_cdtor) (void *, kmem_cache_t *, unsigned long );
Kmem_cache_t * object_cache;
Object_cache = kmem_cache_create ("Object cache", _ size,
_ Offset, _ flags,
(Slab_cdtor) object: ctor,
(Slab_cdtor) bject: dtor );