Возможен ли ощутимый прирост производительности при использовании __assume VC ++? - PullRequest
10 голосов
/ 29 февраля 2012

Возможно ли измеримое повышение производительности при использовании VC ++ __assume? Если да, опубликуйте подтверждение с кодом и контрольными показателями в своем ответе.

Редкая статья MSDN на __assume: http://msdn.microsoft.com/en-us/library/1b3fsfxw(v=vs.100).aspx

В статье упоминается использование __assume(0) для ускорения switch операторов за счет __assume(0) в случае default. Я не измерял увеличение производительности при использовании __assume(0) таким образом:

void NoAssumeSwitchStatement(int i)
{
    switch (i)
    {
    case 0:
        vector<int>();
        break;
    case 1:
        vector<int>();
        break;
    default:
        break;
    }
}

void AssumeSwitchStatement(int i)
{
    switch (i)
    {
    case 0:
        vector<int>();
        break;
    case 1:
        vector<int>();
        break;
    default:
        __assume(0);
    }
}

int main(int argc, char* argv[])
{
    const int Iterations = 1000000;
    LARGE_INTEGER start, middle, end;
    QueryPerformanceCounter(&start);
    for (int i = 0; i < Iterations; ++i)
    {
        NoAssumeSwitchStatement(i % 2);         
    }
    QueryPerformanceCounter(&middle);
    for (int i = 0; i < Iterations; ++i)
    {
        AssumeSwitchStatement(i % 2);
    }
    QueryPerformanceCounter(&end);
    LARGE_INTEGER cpuFrequency;
    QueryPerformanceFrequency(&cpuFrequency);
    cout << "NoAssumeSwitchStatement: " << (((double)(middle.QuadPart - start.QuadPart)) * 1000) / (double)cpuFrequency.QuadPart << "ms" << endl;
    cout << "  AssumeSwitchStatement: " << (((double)(end.QuadPart - middle.QuadPart)) * 1000) / (double)cpuFrequency.QuadPart << "ms" << endl;
    return 0;
}

Округленный вывод консоли, 1000000 итераций:

NoAssumeSwitchStatement: 46 мс
AssumeSwitchStatement: 46 мс

Ответы [ 2 ]

9 голосов
/ 29 февраля 2012

Бенчмарк лжи.Они редко измеряют то, что вы хотите от них.В данном конкретном случае методы, вероятно, были встроенными, и поэтому __assume был просто избыточен.

Что касается самого вопроса, да, это может помочь.Переключение обычно реализуется с помощью таблицы переходов, уменьшая размер этой таблицы или удаляя некоторые записи, компилятор может выбрать лучшие инструкции ЦП для реализации switch.

В вашем крайнем случае, он может превратить switch в if (i == 0) { } else { } структуру, которая обычно эффективна.

Кроме того, обрезка мертвых веток помогает сохранить код чистым, а меньшее количество кода означает лучшее использование кэша инструкций ЦП.

Однако это микрооптимизации, и они редко окупаются: вам нужен профилировщик, чтобы указать их вам, и даже им может быть трудно понять, какое конкретное преобразование сделать (лучше всего __assume)?).Это работа эксперта.

РЕДАКТИРОВАТЬ : в действии с LLVM

void foo(void);
void bar(void);

void regular(int i) {
  switch(i) {
  case 0: foo(); break;
  case 1: bar(); break;
  }
}

void optimized(int i)  {
  switch(i) {
  case 0: foo(); break;
  case 1: bar(); break;
  default: __builtin_unreachable();
  }
}

Обратите внимание, что единственная разница заключается в наличии или отсутствии __builtin_unreachable(), которыйаналогично MSVC __assume(0).

define void @regular(i32 %i) nounwind uwtable {
  switch i32 %i, label %3 [
    i32 0, label %1
    i32 1, label %2
  ]

; <label>:1                                       ; preds = %0
  tail call void @foo() nounwind
  br label %3

; <label>:2                                       ; preds = %0
  tail call void @bar() nounwind
  br label %3

; <label>:3                                       ; preds = %2, %1, %0
  ret void
}

define void @optimized(i32 %i) nounwind uwtable {
  %cond = icmp eq i32 %i, 1
  br i1 %cond, label %2, label %1

; <label>:1                                       ; preds = %0
  tail call void @foo() nounwind
  br label %3

; <label>:2                                       ; preds = %0
  tail call void @bar() nounwind
  br label %3

; <label>:3                                       ; preds = %2, %1
  ret void
}

И обратите внимание, как оператор switch в regular может быть оптимизирован для простого сравнения в optimized.

.следующая сборка x86:

    .globl  regular                  |      .globl  optimized
    .align  16, 0x90                 |      .align  16, 0x90
    .type   regular,@function        |      .type   optimized,@function
regular:                             |    optimized:
.Ltmp0:                              |    .Ltmp3:
    .cfi_startproc                   |            .cfi_startproc
# BB#0:                              |    # BB#0:
    cmpl    $1, %edi                 |            cmpl    $1, %edi
    je      .LBB0_3                  |            je      .LBB1_2
# BB#1:                              |
    testl    %edi, %edi              |
    jne     .LBB0_4                  |
# BB#2:                              |    # BB#1:
    jmp     foo                      |            jmp     foo
.LBB0_3:                             |    .LBB1_2:
    jmp     bar                      |            jmp     bar
.LBB0_4:                             |
    ret                              |
.Ltmp1:                              |    .Ltmp4:
    .size   regular, .Ltmp1-regular  |      .size   optimized, .Ltmp4-optimized
.Ltmp2:                              |    .Ltmp5:
    .cfi_endproc                     |      .cfi_endproc
.Leh_func_end0:                      |    .Leh_func_end1:

Обратите внимание, что во втором случае:

  • код более жесткий (меньше инструкций)
  • есть одно сравнение/ jump (cmpl / je) на всех путях (а не один путь с одним прыжком и путь с двумя)

Также обратите внимание, как это так близко, что я понятия не имею, как что-либо измеритькроме шума ...

С другой стороны, семантически это указывает на намерение, хотя, возможно, assert может лучше подходить только для семантики.

7 голосов
/ 01 марта 2012

Кажется, что это немного меняет, если вы установите правильные ключи компилятора ...

Три запуска следуют.Без оптимизации, выберите скорость и выберите размер.

Этот прогон не имеет оптимизации

C:\temp\code>cl /EHsc /FAscu assume.cpp
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86

assume.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01

/out:assume.exe
assume.obj

C:\temp\code>assume
NoAssumeSwitchStatement: 29.5321ms
  AssumeSwitchStatement: 31.0288ms

Это с максимальными оптимизациями (/ Ox). Обратите внимание, что / O2 был в основном одинаковым по скорости.

C:\temp\code>cl /Ox /EHsc /Fa assume.cpp
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86

assume.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01
/out:assume.exe
assume.obj

C:\temp\code>assume
NoAssumeSwitchStatement: 1.33492ms
  AssumeSwitchStatement: 0.666948ms

Этот прогон должен был минимизировать пространство кода

C:\temp\code>cl -O1 /EHsc /FAscu assume.cpp
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
assume.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01
/out:assume.exe
assume.obj

C:\temp\code>assume
NoAssumeSwitchStatement: 5.67691ms
  AssumeSwitchStatement: 5.36186ms

Обратите внимание, что выходной код сборки согласуется с тем, что Маттиу М. должен был сказать, когда используются опции скорости.Функции переключения вызывались в других случаях.

...