Бить компилятор

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

Я думаю, вопрос в том, почему иногда я могу побить компилятор, а иногда нет? Я получил время 0,006 секунд для operator+= ниже с использованием встроенных функций Intel (против 0,009 при использовании чистого C ++), но время 0,07 с для operator+ используя встроенные функции, в то время как голый C ++ был только 0,03 с.

#include <windows.h>
#include <stdio.h>
#include <intrin.h>

class Timer
{
LARGE_INTEGER startTime ;
double fFreq ;

public:
Timer() {
LARGE_INTEGER freq ;
QueryPerformanceFrequency( &freq ) ;
fFreq = (double)freq.QuadPart ;
reset();
}

void reset() {   QueryPerformanceCounter( &startTime ) ;  }

double getTime() {
LARGE_INTEGER endTime ;
QueryPerformanceCounter( &endTime ) ;
return ( endTime.QuadPart - startTime.QuadPart ) / fFreq ; // as double
}
} ;inline float randFloat(){
return (float)rand()/RAND_MAX ;
}// Use my optimized code,
#define OPTIMIZED_PLUS_EQUALS
#define OPTIMIZED_PLUS

union Vector
{
struct { float x,y,z,w ; } ;
__m128 reg ;

Vector():x(0.f),y(0.f),z(0.f),w(0.f) {}
Vector( float ix, float iy, float iz, float iw ):x(ix),y(iy),z(iz),w(iw) {}
//Vector( __m128 val ):x(val.m128_f32[0]),y(val.m128_f32[1]),z(val.m128_f32[2]),w(val.m128_f32[3]) {}
Vector( __m128 val ):reg( val ) {} // 2x speed, above

inline Vector& operator+=( const Vector& o ) {
#ifdef OPTIMIZED_PLUS_EQUALS
// YES! I beat it!  Using this intrinsic is faster than just C++.
reg = _mm_add_ps( reg, o.reg ) ;
#else
x+=o.x, y+=o.y, z+=o.z, w+=o.w ;
#endif
return *this ;
}

inline Vector operator+( const Vector& o )
{
#ifdef OPTIMIZED_PLUS
// This is slower
return Vector( _mm_add_ps( reg, o.reg ) ) ;
#else
return Vector( x+o.x, y+o.y, z+o.z, w+o.w ) ;
#endif
}

static Vector random(){
return Vector( randFloat(), randFloat(), randFloat(), randFloat() ) ;
}

void print() {

printf( "%.2f %.2f %.2f\n", x,y,z,w ) ;
}
} ;

int runs = 8000000 ;
Vector sum ;

// OPTIMIZED_PLUS_EQUALS (intrinsics) runs FASTER 0.006 intrinsics, vs 0.009 (std C++)
void test1(){
for( int i = 0 ; i < runs ; i++ )
sum += Vector(1.f,0.25f,0.5f,0.5f) ;//Vector::random() ;
}

// OPTIMIZED* runs SLOWER (0.03 for reg.C++, vs 0.07 for intrinsics)
void test2(){
float j = 27.f ;
for( int i = 0 ; i < runs ; i++ )
{
sum += Vector( j*i, i, i/j, i ) + Vector( i, 2*i*j, 3*i*j*j, 4*i ) ;
}
}

int main()
{
Timer timer ;

//test1() ;
test2() ;

printf( "Time: %f\n", timer.getTime() ) ;
sum.print() ;

}

редактировать

Почему я это делаю? Профилировщик VS 2012 говорит мне, что мои векторные арифметические операции могли бы использовать некоторую настройку.

введите описание изображения здесь

4

Решение

Как отмечает Mysticial, профсоюзный взлом является наиболее вероятным виновником test2, Это заставляет данные проходить через кэш L1, который, хотя и быстр, имеет некоторую задержку, которая намного больше, чем ваш выигрыш в 2 цикла, который предлагает векторный код (см. Ниже).

Но также учтите, что процессор может выполнять несколько команд не по порядку и параллельно (суперскалярный процессор). Например, Sandy Bridge имеет 6 исполнительных блоков, p0 — p5, умножение / деление с плавающей запятой выполняется на p0, сложение с плавающей запятой и целочисленное умножение выполняется на p1. Кроме того, деление занимает в 3-4 раза больше циклов, чем умножение / сложение, и не конвейерно (то есть исполнительный модуль не может запустить другую инструкцию, пока выполняется деление). Так в test2в то время как векторный код ожидает завершения дорогостоящего деления и некоторых умножений на блоке p0, скалярный код может выполнять дополнительные 2 инструкции сложения на p1, что, скорее всего, устраняет любое преимущество векторных инструкций.

test1 отличается, постоянный вектор может быть сохранен в xmm зарегистрироваться и в этом случае цикл содержит только инструкцию добавления. Но код не в 3 раза быстрее, чем можно было бы ожидать. Причина в конвейерных инструкциях: каждая команда добавления имеет задержку 3 цикла, но ЦП может запускать новую инструкцию каждый цикл, когда они не зависят друг от друга. Это случай добавления вектора для каждого компонента. Поэтому векторный код выполняет одну инструкцию добавления на каждую итерацию цикла с задержкой в ​​3 цикла, а скалярный код выполняет 3 инструкции добавления, занимая всего 5 циклов (1 запущен на цикл, а третий имеет задержку 3: 2 + 3 = 5).

Очень хороший ресурс по архитектуре и оптимизации процессора http://www.agner.org/optimize/

5

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]