Оператор новой перегрузки и выравнивания
Я перегружаю operator new
, но я недавно столкнулся с проблемой с выравниванием. В основном у меня есть класс IBase
который обеспечивает operator new
а также delete
во всех необходимых вариантах. Все классы происходят от IBase
и, следовательно, также использовать пользовательские распределители.
Проблема, с которой я сталкиваюсь сейчас, заключается в том, что у меня есть ребенок Foo
который должен быть выровнен по 16 байтов, в то время как все остальные в порядке, если выровнены по 8 байтов. Однако мой распределитель памяти выравнивается по 8-байтовым границам только по умолчанию, поэтому теперь код в IBase::operator new
возвращает неиспользуемый кусок памяти. Как это должно быть решено правильно?
Я могу просто принудительно распределить все выделения до 16 байтов, что будет работать до тех пор, пока не появится всплывающий 32-байтовый тип. Выяснение выравнивания внутри operator new
не кажется тривиальным (могу ли я сделать там виртуальный вызов функции для получения фактического выравнивания?) Каков рекомендуемый способ справиться с этим?
я знаю malloc
Предполагается, что он возвращает часть памяти, которая соответствующим образом выровнена для всего, к сожалению, это "все" не включает в себя типы SSE, и мне бы очень хотелось, чтобы это работало, не требуя от пользователя запоминания, какой тип имеет какое выравнивание.
3 ответа
Это возможное решение. Он всегда будет выбирать оператора с самым высоким выравниванием в данной иерархии:
#include <exception>
#include <iostream>
#include <cstdlib>
// provides operators for any alignment >= 4 bytes
template<int Alignment>
struct DeAllocator;
template<int Alignment>
struct DeAllocator : virtual DeAllocator<Alignment/2> {
void *operator new(size_t s) throw (std::bad_alloc) {
std::cerr << "alignment: " << Alignment << "\n";
return ::operator new(s);
}
void operator delete(void *p) {
::operator delete(p);
}
};
template<>
struct DeAllocator<2> { };
// ........... Test .............
// different classes needing different alignments
struct Align8 : virtual DeAllocator<8> { };
struct Align16 : Align8, virtual DeAllocator<16> { };
struct DontCare : Align16, virtual DeAllocator<4> { };
int main() {
delete new Align8; // alignment: 8
delete new Align16; // alignment: 16
delete new DontCare; // alignment: 16
}
Он основан на правиле доминирования: если в поиске есть неоднозначность, и неоднозначность находится между именами производного и виртуального базового класса, вместо него берется имя производного класса.
Возникли вопросы почему DeAllocator<I>
наследуется DeAllocator<I / 2>
, Ответ заключается в том, что в данной иерархии могут быть разные требования к выравниванию, предъявляемые классами. Представь это IBase
не имеет требований к выравниванию, A
имеет 8-байтовое требование и B
имеет 16-байтовое требование и наследует A
:
class IBAse { };
class A : IBase, Alignment<8> { };
class B : A, Alignment<16> { };
Alignment<16>
а также Alignment<8>
оба выставляют operator new
, Если вы сейчас говорите new B
, компилятор будет искать operator new
в B
и найдет две функции:
// op new
Alignment<8> IBase
^ /
\ /
\ /
// op new \ /
Alignment<16> A
\ /
\ /
\ /
B
B -> Alignment<16> -> operator new
B -> A -> Alignment<8> -> operator new
Таким образом, это было бы неоднозначно, и мы не смогли бы скомпилировать: ни один из них не скрывает другой. Но если вы теперь наследуете Alignment<16>
практически из Alignment<8>
и сделать A
а также B
наследовать их практически, operator new
в Alignment<8>
будет скрыто:
// op new
Alignment<8> IBase
^ /
/ \ /
/ \ /
// op new / \ /
Alignment<16> A
\ /
\ /
\ /
B
Это специальное правило сокрытия (также называемое правилом доминирования) работает, только если все Alignment<8>
объекты одинаковы. Таким образом, мы всегда наследуем виртуально: в этом случае есть только один Alignment<8>
(или 16, ...) объект, существующий в любой данной иерархии классов.
mixins - правильный подход, однако перегрузка оператора new - нет. Это выполнит то, что вам нужно:
__declspec(align(256)) struct cachealign{};
__declspec(align(4096)) struct pagealign{};
struct DefaultAlign{};
struct CacheAlign:private cachealign{};
struct PageAlign: CacheAlign,private pagealign{};
void foo(){
DefaultAlign d;
CacheAlign c;
PageAlign p;
std::cout<<"Alignment of d "<<__alignof(d)<<std::endl;
std::cout<<"Alignment of c "<<__alignof(c)<<std::endl;
std::cout<<"Alignment of p "<<__alignof(p)<<std::endl;
}
Печать
Alignment of d 1
Alignment of c 256
Alignment of p 4096
Для gcc используйте
struct cachealign{}__attribute__ ((aligned (256)));
Обратите внимание, что существует автоматический выбор наибольшего выравнивания, и это работает для объектов, размещенных в стеке, объектов, которые являются новыми, и как члены других классов. Он также не добавляет виртуалов и предполагает, что EBCO не имеет дополнительного размера для класса (вне отступов, необходимых для самого выравнивания).
При использовании Visual Studio Express 2010 приведенный выше пример не работает с новым:
CacheAlign *c = new CacheAlign;
выдаст __align_of(c) == 4 (я полагаю, что ожидается), но адрес первого члена CacheAlign также не выровнен в соответствии с запросом.
Не недавний вопрос, но если я правильно понимаю OP, у него есть классы, которые являются дочерними по отношению к родительскому классу, которые определяют allocator & deallocator, и все они могут требовать определенного выравнивания. Что плохого в том, чтобы иметь простой new, который вызывает какой-то частный распределитель, который выполняет фактическую работу и получает аргумент выравнивания - универсальную версию с выравниванием по умолчанию в родительском классе, которая наследуется или перегружается версией, которая задает правильное выравнивание?