简体   繁体   中英

What is the Performance, Safety, and Alignment of a Data member hidden in an embedded char array in a C++ Class?

I have seen a codebase recently that I fear is violating alignment constraints. I've scrubbed it to produce a minimal example, given below. Briefly, the players are:

  • Pool . This is a class which allocates memory efficiently, for some definition of 'efficient'. Pool is guaranteed to return a chunk of memory that is aligned for the requested size.

  • Obj_list . This class stores homogeneous collections of objects. Once the number of objects exceeds a certain threshold, it changes its internal representation from a list to a tree. The size of Obj_list is one pointer (8 bytes on a 64-bit platform). Its populated store will of course exceed that.

  • Aggregate . This class represents a very common object in the system. Its history goes back to the early 32-bit workstation era, and it was 'optimized' (in that same 32-bit era) to use as little space as possible as a result. Aggregate s can be empty, or manage an arbitrary number of objects.

In this example, Aggregate items are always allocated from Pool s, so they are always aligned. The only occurrences of Obj_list in this example are the 'hidden' members in Aggregate objects, and therefore they are always allocated using placement new . Here are the support classes:

class Pool
{
public:
   Pool();
   virtual ~Pool();
   void *allocate(size_t size);
   static Pool *default_pool();   // returns a global pool
};

class Obj_list
{
public:
   inline void *operator new(size_t s, void * p) { return p; }

   Obj_list(const Args *args);
   // when constructed, Obj_list will allocate representation_p, which
   // can take up much more space.

   ~Obj_list();

private:
   Obj_list_store *representation_p;
};

And here is Aggregate. Note that member declaration member_list_store_d :

// Aggregate is derived from Lesser, which is twelve bytes in size
class Aggregate : public Lesser
{
public:
   inline void *operator new(size_t s) {
      return Pool::default_pool->allocate(s);
   }

   inline void *operator new(size_t s, Pool *h) {
      return h->allocate(s);
   }

public:

   Aggregate(const Args *args = NULL);
   virtual ~Aggregate() {};

   inline const Obj_list *member_list_store_p() const;

protected:
   char member_list_store_d[sizeof(Obj_list)];
};

It is that data member that I'm most concerned about. Here is the pseudocode for initialization and access:

Aggregate::Aggregate(const Args *args)
{
   if (args) {
      new (static_cast<void *>(member_list_store_d)) Obj_list(args);
   }
   else {
      zero_out(member_list_store_d);
   }
}

inline const Obj_list *Aggregate::member_list_store_p() const
{
   return initialized(member_list_store_d) ? (Obj_list *) &member_list_store_d : 0;
}

You may be tempted to suggest that we replace the char array with a pointer to the Obj_list type, initialized to NULL or an instance of the class. This gives the proper semantics, but just shifts the memory cost around. If memory were still at a premium (and it might be, this is an EDA database representation), replacing the char array with a pointer to an Obj_list would cost one more pointer in the case when Aggregate objects do have members.

Besides that, I don't really want to get distracted from the main question here, which is alignment. I think the above construct is problematic, but can't really find more in the standard than some vague discussion of the alignment behavior of the 'system/library' new .

So, does the above construct do anything more than cause an occasional pipe stall?

Edit : I realize that there are ways to replace the approach using the embedded char array. So did the original architects. They discarded them because memory was at a premium. Now, if I have a reason to touch that code, I'll probably change it.

However, my question, about the alignment issues inherent in this approach, is what I hope people will address. Thanks!

Ok - had a chance to read it properly. You have an alignment problem, and invoke undefined behaviour when you access the char array as an Obj_list. Most likely your platform will do one of three things: let you get away with it, let you get away with it at a runtime penalty or occasionally crash with a bus error.

Your portable options to fix this are:

  • allocate the storage with malloc or a global allocation function, but you think this is too expensive.
  • as Arkadiy says, make your buffer an Obj_list member:

     Obj_list list; 

but you now don't want to pay the cost of construction. You could mitigate this by providing an inline do-nothing constructor to be used only to create this instance - as posted the default constructor would do. If you follow this route, strongly consider invoking the dtor

list.~Obj_list();

before doing a placement new into this storage.

Otherwise, I think you are left with non portable options: either rely on your platform's tolerance of misaligned accesses, or else use any nonportable options your compiler gives you.

Disclaimer: It's entirely possible I'm missing a trick with unions or some such. It's an unusual problem.

The alignment will be picked by the compiler according to its defaults, this will probably end up as four-bytes under GCC / MSVC.

This should only be a problem if there is code (SIMD/DMA) that requires a specific alignment. In this case you should be able to use compiler directives to ensure that member_list_store_d is aligned, or increase the size by (alignment-1) and use an appropriate offset.

If you want to ensure alignment of your structures, just do a

// MSVC
#pragma pack(push,1)

// structure definitions

#pragma pack(pop)

// *nix
struct YourStruct
{
    ....
} __attribute__((packed));

To ensure 1 byte alignment of your char array in Aggregate

Can you simply have an instance of Obj_list inside Aggregate? IOW, something along the lines of

class Aggregate : public Lesser { ... protected: Obj_list list; };

I must be missing something, but I can't figure why this is bad.

As to your question - it's perfectly compiler-dependent. Most compilers, though, will align every member at word boundary by default, even if the member's type does not need to be aligned that way for correct access.

Allocate the char array member_list_store_d with malloc or global operator new[], either of which will give storage aligned for any type.

Edit: Just read the OP again - you don't want to pay for another pointer. Will read again in the morning.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM