Я оптимизирую конструктор, который вызывается в одном из наших внутренних окружений приложения. Класс, о котором идет речь, имеет ширину около 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.