Предотвращение невыровненных данных в куче
Я строю иерархию классов, которая использует встроенные функции 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-байтовое выравнивание.
До сих пор я могу думать только о двух подходах к решению этой проблемы:
Запрет пользователям MyClass компилировать следующий код:
MyClass myclass;
это означает, что экземпляры MyClass могут создаваться только динамически с использованием оператора new, таким образом гарантируя, что все экземпляры MyClass действительно динамически распределяются с перегруженным новым MyClass. Я проконсультировался в другом потоке о том, как это сделать, и получил несколько отличных ответов: C++, предотвращение создания экземпляра класса в стеке (во время компиляции)
Откажитесь от присутствия членов 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));
}