Подтвердить что ты не робот

Можно ли принудить GCC генерировать эффективные конструкторы для объектов с выравниванием по памяти?

Я оптимизирую конструктор, который вызывается в одном из наших внутренних окружений приложения. Класс, о котором идет речь, имеет ширину около 100 байт, состоит из пучки int s, float s, bool s и тривиальных структур и должен быть тривиально скопируемым (он имеет нетривиальный конструктор по умолчанию, но без деструктора или виртуальные функции). Он построен достаточно часто, что каждая наносекунда времени, потраченная на этот ctor, составляет около 6 000 долларов дополнительного серверного оборудования, которое нам нужно купить.

Однако я обнаружил, что GCC не испускает очень эффективный код для этого конструктора (даже с установкой -O3 -march и т.д.). Выполнение конструктора GCC, заполняя значения по умолчанию через список инициализаторов, занимает около 34 нс. Если вместо этого конструктора по умолчанию я использую написанную вручную функцию, которая записывает непосредственно в пространство памяти объекта с помощью множества встроенных SIMD-указателей и математики указателя, построение занимает около 8ns.

Могу ли я заставить GCC генерировать эффективный конструктор для таких объектов, когда я __attribute__ их выравнивать по памяти на границах SIMD? Или я должен прибегать к методам старой школы, например, писать собственные инициализаторы памяти в сборке?

Этот объект строится только как локальный в стеке, поэтому любые новые служебные данные /malloc не применяются.

Контекст:

Этот класс используется, создавая его в стеке как локальную переменную, выборочно записывая несколько полей с значениями, отличными от значений по умолчанию, и затем передавая его (по ссылке) на функцию, которая передает свою ссылку на другую и так далее.

struct Trivial {
  float x,y,z;
  Trivial () : x(0), y(0), z(0) {};
};

struct Frobozz
{
   int na,nb,nc,nd;
   bool ba,bb,bc;
   char ca,cb,cc;
   float fa,fb;
   Trivial va, vb; // in the real class there several different kinds of these
   // and so on
   Frobozz() : na(0), nb(1), nc(-1), nd(0),
               ba(false), bb(true), bc(false),
               ca('a'), cb('b'), cc('c'),
               fa(-1), fb(1.0) // etc
    {}
} __attribute__(( aligned(16) ));

// a pointer to a func that takes the struct by reference
typedef int (*FrobozzSink_t)( Frobozz& );

// example of how a function might construct one of the param objects and send it
// to a sink. Imagine this is one of thousands of event sources:
int OversimplifiedExample( int a, float b )
{
   Frobozz params; 
   params.na = a; params.fb = b; // other fields use their default values
   FrobozzSink_t funcptr = AssumeAConstantTimeOperationHere();
   return (*funcptr)(params);
}

Оптимальный конструктор здесь будет работать, копируя из статического экземпляра "шаблон" в только что созданный экземпляр, в идеале используя SIMD-операторы для работы по 16 байт за раз. Вместо этого GCC делает то же самое для OversimplifiedExample() — ряд непосредственных mov ops для заполнения структуры by by by by by by by by by by by by by.

// from objdump -dS
int OversimplifiedExample( int a, float b )
{
     a42:55                   push   %ebp
     a43:89 e5                mov    %esp,%ebp
     a45:53                   push   %ebx
     a46:e8 00 00 00 00       call   a4b <_Z21OversimplifiedExampleif+0xb>
     a4b:5b                   pop    %ebx
     a4c:81 c3 03 00 00 00    add    $0x3,%ebx
     a52:83 ec 54             sub    $0x54,%esp
     // calling the 'Trivial()' constructors which move zero, word by word...
     a55:89 45 e0             mov    %eax,-0x20(%ebp)
     a58:89 45 e4             mov    %eax,-0x1c(%ebp)
     a5b:89 45 e8             mov    %eax,-0x18(%ebp)
     a5e:89 45 ec             mov    %eax,-0x14(%ebp)
     a61:89 45 f0             mov    %eax,-0x10(%ebp)
     a64:89 45 f4             mov    %eax,-0xc(%ebp)
     // filling out na/nb/nc/nd..
     a67:c7 45 c4 01 00 00 00 movl   $0x1,-0x3c(%ebp)
     a71:c7 45 c8 ff ff ff ff movl   $0xffffffff,-0x38(%ebp)
     a78:89 45 c0             mov    %eax,-0x40(%ebp)
     a7b:c7 45 cc 00 00 00 00 movl   $0x0,-0x34(%ebp)
     a82:8b 45 0c             mov    0xc(%ebp),%eax
     // doing the bools and chars by moving one immediate byte at a time!
     a85:c6 45 d0 00          movb   $0x0,-0x30(%ebp)
     a89:c6 45 d1 01          movb   $0x1,-0x2f(%ebp)
     a8d:c6 45 d2 00          movb   $0x0,-0x2e(%ebp)
     a91:c6 45 d3 61          movb   $0x61,-0x2d(%ebp)
     a95:c6 45 d4 62          movb   $0x62,-0x2c(%ebp)
     a99:c6 45 d5 63          movb   $0x63,-0x2b(%ebp)
     // now the floats...
     a9d:c7 45 d8 00 00 80 bf movl   $0xbf800000,-0x28(%ebp)
     aa4:89 45 dc             mov    %eax,-0x24(%ebp)
     // FrobozzSink_t funcptr = GetFrobozz();
     aa7:e8 fc ff ff ff       call   aa8 <_Z21OversimplifiedExampleif+0x68>
     // return (*funcptr)(params);
     aac:8d 55 c0             lea    -0x40(%ebp),%edx
     aaf:89 14 24             mov    %edx,(%esp)
     ab2:ff d0                call   *%eax
     ab4:83 c4 54             add    $0x54,%esp
     ab7:5b                   pop    %ebx
     ab8:c9                   leave 
     ab9:c3                   ret   
}

Я попытался побудить GCC создать один "шаблон по умолчанию" этого объекта, а затем выполнить массовое копирование в конструкторе по умолчанию, совершив немного обмана со скрытым конструктором 'dummy', который сделал базовый экземпляр и затем по умолчанию просто скопируйте его:

struct Frobozz
{
     int na,nb,nc,nd;
     bool ba,bb,bc;
     char ca,cb,cc;
     float fa,fb;
     Trivial va, vb;
     inline Frobozz();
private:
     // and so on
     inline Frobozz( int dummy ) : na(0), /* etc etc */     {}
} __attribute__( ( aligned( 16 ) ) );

Frobozz::Frobozz( )
{
     const static Frobozz DefaultExemplar( 69105 );
     // analogous to copy-on-write idiom
     *this = DefaultExemplar;
     // or:
     // memcpy( this, &DefaultExemplar, sizeof(Frobozz) );
}

Но это сгенерировало еще более медленный код, чем базовый по умолчанию список инициализаторов, из-за некоторого избыточного копирования копий.

Наконец, я прибегнул к написанию свободной функции для выполнения шага *this = DefaultExemplar, используя встроенные функции компилятора и предположения о выравнивании памяти для выпуска конвейерно MOVDQA Команды SSE2, которые эффективно копируют структуру. Это дало мне представление, которое мне нужно, но это нехорошо. Я думал, что мои дни написания инициализаторов в сборке были позади меня, и я бы предпочел, чтобы оптимизатор GCC испускал правильный код в первую очередь.

Есть ли способ заставить GCC генерировать оптимальный код для моего конструктора, некоторые настройки компилятора или дополнительные __attribute__ Я пропустил?

Это GCC 4.4 работает на Ubuntu. Флаги компилятора включают -m32 -march=core2 -O3 -fno-strict-aliasing -fPIC (среди прочих). Переносимость не является предметом рассмотрения, и я полностью готов пожертвовать стандартами для выполнения здесь.

Сроки выполнялись путем непосредственного считывания счетчика временных меток с помощью rdtsc, например, для измерения цикла N OversimplifiedExample() вызовов между образцами с должным вниманием к разрешению таймера и кешу и статистической значимости, и поэтому на.

Я также оптимизировал это, уменьшив количество сайтов звонков, насколько это возможно, но я все равно хотел бы знать, как вообще получить лучшие ctors из GCC.

4b9b3361

Ответ 1

Вот как я это сделаю. Не объявлять конструктор; вместо этого объявите фиксированный Frobozz, который содержит значения по умолчанию:

const Frobozz DefaultFrobozz =
  {
  0, 1, -1, 0,        // int na,nb,nc,nd;
  false, true, false, // bool ba,bb,bc;
  'a', 'b', 'c',      // char ca,cb,cc;
  -1, 1.0             // float fa,fb;
  } ;

Тогда в OversimplifiedExample:

Frobozz params (DefaultFrobozz) ;

С gcc -O3 (версия 4.5.2) инициализация params сводится к:

leal    -72(%ebp), %edi
movl    $_DefaultFrobozz, %esi
movl    $16, %ecx
rep movsl

который примерно так же хорош, как и в 32-разрядной среде.

Предупреждение: Я пробовал это с 64-разрядной версией g++ версии 4.7.0 20110827 (экспериментальный), и он генерировал явную последовательность 64-битных копий вместо перемещения блока. Процессор не позволяет rep movsq, но я ожидаю, что rep movsl будет быстрее, чем последовательность 64-разрядных нагрузок и хранилищ. Возможно нет. (Но переключатель -Os - оптимизация для пробела - использует инструкцию rep movsl.) В любом случае попробуйте это и сообщите нам, что произойдет.

Отредактировано для добавления: Я ошибся в том, что процессор не разрешил rep movsq. Документация Intel гласит: "Команде MOVS, MOVSB, MOVSW и MOVSD может предшествовать префикс REP", но, похоже, это просто сбой в документации. В любом случае, если я делаю Frobozz достаточно большим, тогда 64-битный компилятор генерирует команды rep movsq; поэтому он, вероятно, знает, что он делает.