оператор новой перегрузки и выравнивания - PullRequest
34 голосов
/ 03 марта 2010

Я перегружаю operator new, но недавно у меня возникла проблема с выравниванием. По сути, у меня есть класс IBase, который обеспечивает operator new и delete во всех необходимых вариантах. Все классы являются производными от IBase и, следовательно, также используют пользовательские распределители.

Проблема, с которой я сейчас сталкиваюсь, заключается в том, что у меня есть дочерний элемент Foo, который должен быть выровнен по 16 байтам, в то время как все остальные в порядке, если выровнены по 8-байтам. Однако мой распределитель памяти выравнивается по 8-байтовым границам только по умолчанию, поэтому теперь код в IBase::operator new возвращает неиспользуемый кусок памяти. Как это должно быть решено правильно?

Я могу просто принудительно назначить все выделения 16 байтам, что будет работать до тех пор, пока не появится 32-байтовый тип с выравниванием. Выяснение выравнивания внутри operator new не кажется тривиальным (могу ли я сделать там виртуальный вызов функции для получения фактического выравнивания?) Каков рекомендуемый способ справиться с этим?

Я знаю, malloc должен возвращать часть памяти, которая выровнена соответствующим образом для всего, к сожалению, это "все" не включает типы SSE, и я бы очень хотел, чтобы это работало, не требуя от пользователя помните, какой тип имеет какое выравнивание.

Ответы [ 2 ]

21 голосов
/ 03 марта 2010

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

#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, ...) объект.

7 голосов
/ 06 марта 2010

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 нет дополнительного размера для класса (за исключением отступов, необходимых для самого выравнивания).

...