Предотвращение невыровненных данных в куче

Я строю иерархию классов, которая использует встроенные функции SSE, и поэтому некоторые члены класса должны быть выровнены по 16 байтов. Для экземпляров стека я могу использовать __declspec(align(#)), вот так:

typedef __declspec(align(16)) float Vector[4];
class MyClass{
...
private:
Vector v;
};

Теперь, так как __declspec(align(#)) директива компиляции, следующий код может привести к невыровненному экземпляру Vector в куче:

MyClass *myclass = new MyClass;

Это также, я знаю, я могу легко решить, перегрузив операторы new и delete для использования _aligned_malloc а также _aligned_free соответственно. Вот так:

//inside MyClass:
public:
void* operator new (size_t size) throw (std::bad_alloc){
    void * p = _aligned_malloc(size, 16);
    if (p == 0)  throw std::bad_alloc()
    return p; 
}

void operator delete (void *p){
    MyClass* pc = static_cast<MyClass*>(p); 
    _aligned_free(p);
}
...

Пока все хорошо.. но вот моя проблема. Рассмотрим следующий код:

class NotMyClass{ //Not my code, which I have little or no influence over
...
MyClass myclass;
...
};
int main(){
    ...
    NotMyClass *nmc = new NotMyClass;
    ...
}

Так как экземпляр myclass MyClass создается статически на динамическом экземпляре NotMyClass, myclass будет выровнен на 16 байт относительно начала nmc из-за вектора __declspec(align(16)) директивы. Но это бесполезно, поскольку nmc динамически размещается в куче с помощью нового оператора NotMyClass, который не обязательно (и, вероятно, вероятно, НЕ) 16-байтовое выравнивание.

До сих пор я могу думать только о двух подходах к решению этой проблемы:

  1. Запрет пользователям MyClass компилировать следующий код:

    MyClass myclass;
    

    это означает, что экземпляры MyClass могут создаваться только динамически с использованием оператора new, таким образом гарантируя, что все экземпляры MyClass действительно динамически распределяются с перегруженным новым MyClass. Я проконсультировался в другом потоке о том, как это сделать, и получил несколько отличных ответов: C++, предотвращение создания экземпляра класса в стеке (во время компиляции)

  2. Откажитесь от присутствия членов Vector в моем классе, и у меня будут только указатели на Vector в качестве членов, которые я буду распределять и отменять, используя _aligned_malloc а также _aligned_free в ctor и dtor соответственно. Этот метод кажется грубым и подверженным ошибкам, поскольку я не единственный программист, пишущий эти классы (MyClass является производным от базового класса, и многие из этих классов используют SSE).

Однако, поскольку оба решения были осуждены в моей команде, я прихожу к вам за предложениями другого решения.

3 ответа

Решение

Если вы настроены на выделение кучи, другой идеей является избыточное выделение в стеке и ручное выравнивание (ручное выравнивание обсуждается в этом посте SO). Идея состоит в том, чтобы выделить байтовые данные (unsigned char) с размером, гарантированно содержащим выровненную область необходимого размера (+15), затем найдите выровненную позицию, округляя вниз от наиболее смещенной области (x+15 - (x+15) % 16, или же x+15 & ~0x0F). Я разместил рабочий пример этого подхода с векторными операциями на кодовой панели (для g++ -O2 -msse2). Вот важные биты:

class MyClass{
   ...
   unsigned char dPtr[sizeof(float)*4+15]; //over-allocated data
   float* vPtr;                            //float ptr to be aligned

   public:
      MyClass(void) : 
         vPtr( reinterpret_cast<float*>( 
            (reinterpret_cast<uintptr_t>(dPtr)+15) & ~ 0x0F
         ) ) 
      {}
   ...
};
...

Конструктор обеспечивает выравнивание vPtr (обратите внимание, что порядок членов в объявлении класса важен).

Этот подход работает (распределение кучи / стека содержащих классов не имеет отношения к выравниванию), является портируемым (я думаю, что большинство компиляторов предоставляют указатель размером с указатель uintptr_t), и не будет пропускать память. Но это не особенно безопасно (быть уверенным, что выровненный указатель действителен при копировании и т. Д.), Тратит (почти) столько памяти, сколько использует, и некоторые могут счесть reinterpret_casts неприятным.

Риски проблем с выровненными операциями / не выровненными данными можно было бы в основном устранить, инкапсулировав эту логику в объект Vector, тем самым управляя доступом к выровненному указателю и гарантируя, что он выравнивается при построении и остается действительным.

Вы можете использовать "размещение новых".

void* operator new(size_t, void* p) { return p; }

int main() {
    void* p = aligned_alloc(sizeof(NotMyClass));
    NotMyClass* nmc = new (p) NotMyClass;
    // ...

    nmc->~NotMyClass();
    aligned_free(p);
}

Конечно, вы должны быть осторожны, уничтожая объект, вызывая деструктор, а затем освобождая пространство. Вы не можете просто позвонить удалить. Вы можете использовать shared_ptr<> с другой функцией для автоматического решения этой проблемы; это зависит от того, является ли для вас проблемой работа с shared_ptr (или другой оболочкой указателя).

Будущий стандарт C++0x предлагает средства для работы с необработанной памятью. Они уже включены в VC++2010 (в рамках tr1 Пространство имен).

std::tr1::alignment_of // get the alignment
std::tr1::aligned_storage // get aligned storage of required dimension

Это типы, вы можете использовать их так:

static const floatalign = std::tr1::alignment_of<float>::value; // demo only

typedef std::tr1::aligned_storage<sizeof(float)*4, 16>::type raw_vector;
        // first parameter is size, second is desired alignment

Тогда вы можете объявить свой класс:

class MyClass
{
public:

private:
  raw_vector mVector; // alignment guaranteed
};

Наконец, вам нужно некоторое приведение, чтобы манипулировать им (это сырая память до сих пор):

float* MyClass::AccessVector()
{
  return reinterpret_cast<float*>((void*)&mVector));
}
Другие вопросы по тегам