Wild <Programming>: Type Cache (III)

zhaozj2021-02-16  61

Wide : Type Cache (III) Andrei AlexandRescu

This is the last part of the type of cache-lightweight and flexible continuous arbitrary type object. It is positioned between basic within and complex std :: vectors, when efficient is important. Types of cache is a very useful structure. More importantly, it can create more complex structures as a convenient basic component - such as String, Vector, Queue, and others. The previous part [1] focuses on the basic implementation of the basic implementation of Buffer operation, such as filling and copying memory. This article you have to read has a wider perspective - we want to discuss copy and mobile objects, rather than raw data.

Low-energy dispenser (allocator) When I want to use discussions based on strategy-based design [2], I often laugh at the STL dispenser is a famous experiment, but it is a tragic failure experiment. . Just as Freud pointed out that middle-aged anxiety is related to the childhood of the childhood, the interesting story of C memory allocation needs to be traced back to C. From the beginning, C provides a functional function of three memory management: Malloc, Free and Realloc

Void * malloc (size_t size); void free (void * p); void * realloc (void * p, size_t newsize);

I may be a magical number, but the three memory management functions are far less enough. If you take into account the annoying repetition --Realloc can be a top three - the situation is the case:

* If you pass the empty pointer, Realloc works in Malloc * If you pass a zero size, Realloc works free.

So, Realloc is a full-power function, which can handle all the needs of all memory management. (Shun, this is a disadvantage) Let's look at a typical Realloc implementation:

Void * Realloc (void * p, size_t news {// exclude extreme case if (p == 0) return malloc (newsize); if (news == 0) Return Free (p), 0;

// Try to expand IF on site (p == _expand (p, newsize) && _msize (p)> = news) {Return P;}

// Need to move memory - get the new memory block Void * pnew = malloc (news); if (! PNEW) return 0;

// Move data to new position Memcpy (PNEW, P, std :: min (_msize (p), newsing); free (p); return pnew;}

I think the realoc implementation above is easy to read, especially because it does not follow See (single entrance, single export Single Entry, Single EXIT) rules [3]. But some details are not very clear - what is the _ Expand and _msize? These two functions are Tools for Realloc:

Void * expand (void * p, sizet news; size_t _ms);

_expand attempts to expand the memory block that is pointed to by its first parameter. It is possible that when you want to redistribute a memory, it is likely to be available. In this case, the memory distributor can quickly adjust its recording memory used to reflect the new memory. This is much faster than copying the entire block to a bigger location. When successful, _expand returns the pointer to it. _MSIZE only returns the size of the memory block pointing to it. (All memory manager must know the size of each memory block) _expand and _msize are non-standard, so you can only use them through Realloc. Go back to Realloc, if the expansion failed on site, Realloc assigns a whole new memory block. Use Memcpy to copy the old memory block to the new memory block, free out of the old memory block. very simple. Maybe too simple. When C is born, it relies very dependent on C library and basic functional functions. In particular, if C develops its own, a separate memory distributor is very difficult - establishing a new, strong type allocation mechanism is more meaningful on the standard C distributor. And this is the problem. Although Memcpy is so loyal to C, it is C good but clumsy servant. For C, moving things with Memcpy works very well, but C needs more. The object with constructor and the destructuring function cannot be moved in memory. They may be packaged in data that points to themselves or compiler (such as pointers that point to base classes when the virtual success). This means that if a C object may no longer be effective from one at Memcpy to the other - fairly, the C standard clearly prohibits Memcpy not a POD (simple and old type). If Memcpy is not good enough, then Realloc is not good - so it does not give redistribution from C from the first day. C has new and delete, but there is no "renew" or other things similar to Realloc. Such std :: allocator (it assigns another thing is the troubles of bringing everyone) nor does it offer any redistribution interfaces. So in the second millennium twilight, most modern C libraries do not have a means to support optimized memory allocation. Because C lacks efficient redistribution, the C program is destined to use more memory and / or the speed that should be reached. C and C are not external support_msize, so a dynamically allocated memory size is usually stored twice in actual application - inside the dispenser and in the program itself, this is more in the wound. Please temporarily close your eyes now. Close your eyes imagine you around a variety of C applications to run (or crash) - all of these desktop applications, commercial applications, servers, multi-storey, embedded systems, and others you can think of. Now open your eyes face reality. They are not optimized: they do not need to copy memory when copying memory, and they manage information that does not require them. All of this is due to the design of C's original dispensers that do not provide access to customers [4]. We now look at how we optimize the buffer's memory allocation. Because the type cache uses the allocator, we need a good solution to a STD :: Allocator interface.

The mallocator is not, this is not a good dock movie name (the authors think it is very elephant? Is it true?) - He is just a mall-based mall-based distributor. We started from a fact that Realloc is not completely useless in C , it is just unable to use a part of C . Therefore, even if you lack enough standard C API, you can safely use Realloc to any POD, including all basic types. This is in our Buffer class, in order to achieve a fast redistribution, we need the following premise: a) Know if an arbitrary type T is a POD. B) Define a malloc-based alligator, which has the same interface and increase the redistribution function. For POD, this new distributor uses Realloc directly, for non-POD, it returns "No", in which case the caller must perform a typical allocation - copy-release series actions. C) Provide a method to ask a dispenser: "Can you redistribute?" Std :: allocator to this question "No." The point is, you can't std :: allocator. D) If possible, try to use Reallocate as possible in Buffer. That is to say, if you instantiate buffe >, using a general practice. But if it is buffer >, the buffer uses high efficiency redistribution as much as possible. Each of the above matters has a unique best solution in modern C . How do you practice this as an exercise before going to the next reading? Ok, announce the answer. A) If you read [5], you know Typetraits is a solution. Add something on the basis of [5], you can write this now.

Namespace Typetraits {Template struct ispod {enum {value = isprimitive :: value};

By default, only basic types are POD, just as shown in [5], you can know the type of Typetraits :: ISPODB) Herb and Jim that you know to be a POD type, you might write: "Prophet Austern has been detailed How to write a compatible distributor. "In fact, Matt Austern did in [6], this you can find it online in universal interconnects.

Template Struct Mallocator {... Using Malloc / Free Distributor Implementation, see [6] ... T * Reallocate (t * p, size_type newsize) {Return Typetraits :: ispod :: Value ? static_cast (Realloc (p, newsize): static_cast (0);}}:

C) How do I query a type of function without changing its internal code? Of course, --traits [7]! This doesn't need to spend any strength:

Template struct allocatorsupportsReallocation {enum {value = false};

Template Struct AllocatorSupportsReallocation > {enum {value = true};}; d) Write a function relies on a Boolean value to distinguish between two overload versions can be used by int2type [2 ,8] It is easy to do. Treatment is the best means by overloading

template typename A :: pointer Reallocate (A & alloc, typename A :: pointer p; typename A :: size_type oldObjectCount, tyoename A :: size_type newObjectCount, Int2Type ) {.... do not support heavy the dispenser dispensed achieve reallocation ...} template ) {.... Using the allocator that supports the redistribution to achieve redistribution ....

Why do you need an OldobjectCount parameter? Very simple: When A :: Realloc returns zero, then you need to assign a new memory block and copy the object, you need to know how many objects you have. This is why two reallocate functions implement the number of objects, not the number of bytes. The Reallocate compares the member function of the standard distributor is a function of a slightly higher level. Now when you call Reallocate, all buffer need to pass int2type :: Value> as the last parameter, then, look! Fast redistribution works! Sometimes you do have access to the _wxpand and the _msize library expansion function. Microsoft's C library is an example of this. This means that you can redistribute all types efficiently, not just POD. Of course, if you do this, you have to make your code have a cross-platform ability, you must use #ifdef.

Mobile Objects We are now going to another topic. Even if you can quickly redistribute memory by means of any means of expanding the ground, this is still far away from the optimization. Consider the following:

Buffer

Now no matter whether you use Mallocator, sometimes you have to experience allocation - copy - destroyed loops. Assume that this action happens, the RESERVE call includes mobile 1,000,000 string objects. In C , the approach of moving objects is similar to those basic transfer methods: at another clone object and destroy source objects. Clone All these strings may overhead huge - if the string is not a reference number. This is much larger than only copying their memory - this is very likely [9] - Su RESERVE brings a lot of unnecessary work, reserve allocates a new memory block, copy each string to new blocks , Then finally destroy the string in the old block. But why did we actually need to replace the object easily, but do all of these copy movements? We only need to tell a string: "Hey, you have a new location in the store, please re-place it yourself." Because the old position of the string is already useless (no matter how it will be discarded), the string can be quickly Copy their internal pointer to the target memory block. We don't need to think that you can understand that you need to make movements as a basic operation, and a copy is copied. Mobile has no replication, the total number of object remains unchanged. In the absence of this concept, we have achieved it by cloning in C and destroying the original object. We need to do more smart. There are many ways to move. The simplest one can use functions Template void move (t * src, void * dest);

But this is not that simple. Suppose you are implementing a string class:

Class UltimateString {char * end_; char * end /; char * endofStorage_; // Because you can't use _msize, sigh .... public: .... Other functions ....}; now assume you UltimateString Move, as follows

template <> void Move (UltimateString * src, void * dest) {UltimateString * typedDest = static_cast (dest); typedDest-> start_ = src-> start_; typedDest-> end_ = src-> end_; typedDest- > endofstorage_ = src-> endofStorage_;}

If you think this is ok, then you are wrong. This is not possible, because the standard gives the compiler to add their own data to the freedom in your class (unless they are POD). That is to say: The compiler may add an int __coolnessfactor_ member variable in your string class, a member variable you don't know, let you copy all things from SRC to Dest to become impossible. The result of Move is unpredictable. C emphasizes that each constructor must be created by calling one of these constructors. Other ways to create objects are prohibited. So the mobile object must be implemented by some constructor. TEPLATE Struct Takeover {Explicit Takeover (T & Obj): OBJ_ (OBJ) {} T & Get () {Return Obj_;} private: T & Obj_;

Takeover wraps a reference in an object and provides access to it. Then, you implement a UltimateString constructor, which accepts a Takeover object: class ultimateString {char * start_; char * end_; char * endofStorage_; // Because you can't use _msize, again sigh .... public : UltimateString (Takeover wrap) {UltimateString & src = wrap, Get (); start_ = src.start_; end_ = src.end_; endOfStorage_ = src.endOfStorage_; // Clear the source pointer src.start_ = src.end_ = Src.endofStorage_ = 0;} ....}; For convenience, we call the above constructor "takeover constructor". The tube constructor coexists with other constructors. It copies three pointers into the constructed string object, then clear the source pointer. We need the last step because the tube constructor must make the source object in a destroyable state. Everything is fine so far. However, how do you know that a type t in a generic code implements a constructor that accepts the Takeover parameter? Based on this, you need to have a strategy for calling different mobile objects at compile time. This is the most interesting part of this article, because I can have the opportunity to let you see [2] and [8]. Remember Conversion template? If you don't remember, I believe me, it is worth seeing, so print [8] and read, or better (at least to me), buy this [2]. Conversion provides a means of compiling whether it can be converted to a certain type of T. If you change u to Takeover to change T, Well, or t, you will find this is our problem solution. Any code can be known from the outside whether any type is implemented to implement a tube constructor. About unexpected security. Generally moving an object should not be thrown, because the movement does not involve allocating new resources. But this is not always true. Consider the following code: Class Widget {.... Do not implement a tube constructor .... Copy constructor may throw unexpected ....

Class Midget {Widget W_; PUBLIC: Midget (Takeover Wrap) {... Ah, how do you implement it without throwing an accident? ....}};

This problem can be avoided in two ways. One is very simple, replace the widget member with widget * (or you have enough strength, replace it with std :: auto_ptr ) and use dynamic assignment. The pointer can be copied by a clean and land without copying the widget itself, so the constructor is easy to write. The second method of implementing a tube constructor in the midget requires two different functions of the Widget. 1. Widget must implement a SWAP that does not thrown, just like the container in all standard libraries. It also creates a (very uncomfortable) framework for other types to define SWAP. Most important, standard library demonstrates non-thrown object exchanges is a basic functional operation. SWAP is very useful in a variety of circumstances, and cannot be implemented in an external implementation of the class. If you know the standard, you may implement SWAP in all of your level (non-polymorphism) object. (If lucky, this article may convince you also implement a tube constructor) 2. The default constructor of Widget does not throw an accident. This is usually very easy. If you meet the two conditions above, you can implement the takeover constructor:

Midget (TakeOver Wrap): w_ () // Telegraph W Call the default constructor {w_.swap (wrap.get (). W_);}

This is to create a null value of a target object, exchange with a null value and the value of the subject being taken. Take over constructor and exchange are two related and slightly repeated operations, and the relationship between them is as follows:

* You can implement a tube constructor with a SWAP that does not thrown, but only you have a constructor that does not thrown. If all your constructor may throw, SWAP is unable to do use to implement the takeover constructor. * If there is no @ #!% ^ & (On this point, you block several text) alignment issues, you can use a tube constructor to implement SWAP.

Void Swapviamove (Midget & lhs, Midget & rhs) {char buffer [sizeof (midget)] / / must be aligned to store a midget // copy LHS to Buffer Midget * lhsmoved = new (buffer) Midget (Takeover (LHS)) // Mobile RHS to LHS LHS. ~ Midget (); new (& lhs)); // Mobile LHS (now in Buffer) to RHS RHS. ~ Midget (); new & r Hs (Midget (Takeover (* lhsmoved)); lhsmoved-> ~ midget ();

This function is incorrect (and looks very strange), because there is no 100%-pointed method to ensure that Buffer is correctly aligned to place a Widget. If you can access the constructor that does not thrown, you can remove the alignment problem like this:

Void SwapViamove (Midget & lh, Midget & r Hs) {midget temp; // does not throw // moving LHS to TEMP TEMP. ~ MIDGET (); new (& TEMP) Midget (Takeover (LHS)); // Mobile RHS To LHS LHS. ~ Midget (); new (& lhs)); // Mobile Temp to RHS RHS. ~ Midget (); new (& rhs) Midget (Takeover (TEMP) ); //Temp.~midget (); do not need, because the compiler will do} (if you want to declare the Temp to save some steps, you will lose this function's re-enrichment (Reentrant "Conclusion: Any SWAP and tube constructors that do not thrown can be freely implemented, when and only when you can access a constructor. Now considering the moving object sequence, the most effective algorithm is like this:

* If the type is POD, you will use Memcpy, Memmove, or [1], one of the fastest copy mode. Duff may say: I didn't listen to it. :) * Otherwise, if the type supports the tube constructor, use it * Otherwise, the special clone in the cycle - destroying the way.

SUMMARY AND Conclusion The memory allocation method generally used in C and C is not optimized. The C library cannot expand a memory in place, and the result C also does not support these. In addition, C and C do not provide a size information of a memory block, but this information distributor can be obtained. These two defects affect the speed and footprint of the application. I don't know what extent, I suspect that the impact on most applications is small or ignored, and it is very troublesome for some procedures. In order to overcome the shortcomings mentioned, you can design a mallocator, which uses Realloc to POD. This allows the portion in your object to benefit from fast redistribution. If your C library implementation provides extension features similar to _msize and _expand, you can use these in your mallocator implementation, so that all objects perform fast redistribution. For C objects, the movement that does not thrown is a basic operation like a copy. Swap objects are conceptually some degree of moving (in a particular environment). A reliable mobile object method is to use a tube constructor. Whether you do it, there is a means of moving objects (fast and non-thrown accidents) to make a feasible way to make a composite container similar to Vector >. [1] Andrei Alexandrescu. "Generic : Typed Buffers," C / C User C Experts Forum, October 2001, . [2] Andrei Alexandrescu. Modern C Design (Addison-Wesley Longman, 2001). This is not the first book that is based on strategy-based design, but this book is systematically recorded and uses such a design. [3] SESE is a widely propagated spell in the structured programming era. In the SESE-high-level country, each function must have only one entry (no matter what, most languages ​​are designed) and an exit. "Single Exit" is limited to today, this unexpected era is already Chen Yu. Although it has been repeatedly certified, the SESE code is longer, more difficult to maintain, more complex, and more efficient than equivalent, more export code. There are still many poor souls firmly believe that SESE is great. (After all, Aristotle believes that the sun flying angel dragged the sky) SESE's fanatics formed a scale in the Pascal and C era. Unfortunately, these people are today's development manager with age. [4] Microsoft's C library implementation is indeed access to _expand and _msize. You can say that you want, but the people who have of Redmond's name are actual.

[5] Andrei Alexandrescu. "Generic : type buffers (i)," C / C Uses Journal 200, Experts Forum, August 2001, . [6] Austern, Matt. "The Standard Librarian: What Are Allocators Good for?" C / C Uses Journal C Experts Forum, December 2000, . [7] Andrei AlexandRescu. "Traits: The else-if-dam of type," C Report, April 2000. [8] Andrei Alexandrescu. "Generic : mappings Between Types and Values," C / C Uses Journal C Experts Forum, October 2000, . [9] Andrei Alexandrescu. "Generic : a policy", C / C Uses Journal C Experts Forum, June 2001, .andrei Alexandrescu is a doctor in Washington, Seattle, is also the author of the book "Modern C Design" book. You can contact him via www.moderncppdesign.com. Andrei is also a C seminar (). A superior lecturer.

You can get this source code from the CUJ website or http://merced.go.nease.net/code/buffer.zip.

转载请注明原文地址:https://www.9cbs.com/read-23038.html

New Post(0)