Будет ли компилятор разворачивать этот цикл?

Я создаю многомерный вектор (математический вектор), в котором я разрешаю основные математические операции +,-,/,*,=. Шаблон принимает два параметра: один — это тип (int, float и т. д.), а другой — размер вектора. В настоящее время я применяю операции через цикл for. Теперь, учитывая, что размер известен во время компиляции, будет ли компилятор разворачивать цикл? Если нет, есть ли способ развернуть его без (или с минимальным) снижением производительности?

template <typename T, u32 size>
class Vector
{
public:
    // Various functions for mathematical operations. 
    // The functions take in a Vector<T, size>.
    // Example:
    void add(const Vector<T, size>& vec)
    {
        for (u32 i = 0; i < size; ++i)
        {
            values[i] += vec[i];
        }
    }

private:
    T   values[size];
};

Прежде чем кто-то прокомментирует Profile then optimize, обратите внимание, что это основа моего движка 3D-графики, и он должен быть быстрым. Во-вторых, я хочу знать ради самообразования.


person Samaursa    schedule 26.05.2011    source источник
comment
Не хочу показаться странным, но если вы скомпилируете с оптимизацией и выгрузите сборку, вы можете многое узнать :)   -  person Skurmedel    schedule 26.05.2011
comment
Даже если это должно быть очень быстро, профилирование — бесценный инструмент. На самом деле, это ваш лучший друг, особенно если нужно быть очень быстрым, потому что это работает намного лучше, чем угадывание. (Кроме того, ответ может сильно зависеть от компилятора, используемых флагов и, возможно, даже больше.)   -  person    schedule 26.05.2011
comment
If not, is there a way to unroll it with no (or minimal) performance penalty? Весь смысл развертывания цикла заключается в повышении производительности... зачем вам это делать, если это снижает производительность?   -  person Chad    schedule 26.05.2011
comment
Незначительное исправление: это не скомпилируется, потому что в объявлении вашего класса отсутствует точка с запятой. Также довольно стандартно использовать camelCase для методов.   -  person Seth Johnson    schedule 26.05.2011
comment
Дополнительное замечание: почему вы используете u32 (что бы это ни было, но я могу сделать обоснованное предположение) вместо size_t для размера объекта?   -  person Fred Foo    schedule 26.05.2011
comment
Кроме того, ваш код не добавляет, он назначает.   -  person Seth Johnson    schedule 26.05.2011
comment
Обратите внимание, что развертывание цикла не обязательно является выигрышным, особенно на более поздних процессорах x86.   -  person Paul R    schedule 26.05.2011
comment
@larsman: Если у вас более 4 миллиардов точек в трехмерной модели, у вас больше проблем, чем при использовании 32-битного счетчика. (Кроме того, драйвер графического процессора, вероятно, жестко запрограммировал 32-битный счетчик.)   -  person Ben Voigt    schedule 26.05.2011
comment
Это действительно пахнет преждевременной оптимизацией. Сначала заставьте ваш графический движок работать. Затем профилируйте и оптимизируйте. Я предполагаю, что этот фрагмент кода не станет вашим узким местом.   -  person Bart    schedule 26.05.2011
comment
@Paul: Развертки полного цикла может и не быть, но небольшой коэффициент развертки, такой как 4x, скорее всего, есть. Особенно, если операции SSE можно использовать для копирования данных.   -  person Ben Voigt    schedule 26.05.2011
comment
@Bart: Вы не хотите сказать, что оператор добавления вектора должен фактически добавлять (а не просто копировать) свой операнд, не так ли? Следующее, что вы знаете, это то, что люди будут проповедовать правильность.   -  person Ben Voigt    schedule 26.05.2011
comment
@ Бен Войт Никогда. Разве Дийкстра не говорил о компетентном программисте, поэтому он подходит к своей задаче с полным смирением и избегает правильности, как чумы?... ;)   -  person Bart    schedule 26.05.2011
comment
@Bart: ооо, это идеальная цитата для этого - если вы цитируете правильно, вы избегаете хитрых уловок, а если вы цитируете умно, вы избегаете правильности. Чистая победа.   -  person Ben Voigt    schedule 26.05.2011
comment
@Bart: Двигатель работает. Я работал со сторонней математической библиотекой и переключаюсь на свои собственные классы. Это вопрос, который пришел мне в голову, когда я писал векторные классы (состоящие из Vector2, Vector3 и Vector4), и мне было интересно, смогу ли я обойтись без Vector, способного удовлетворить все упомянутые типы Vector.   -  person Samaursa    schedule 26.05.2011
comment
Вы даже не должны задавать вопрос. Компилятор (при оптимизации по скорости) выберет оптимальный способ сделать это. Оптимальным способом может быть не разворачивание петли. Компилятор хорош в этих вещах, пусть работает наилучшим образом. Попытка оптимизировать его самостоятельно только сделает код медленнее (вы можете сравняться с компилятором, но вряд ли когда-нибудь превзойдете его).   -  person Martin York    schedule 26.05.2011


Ответы (6)


Вы можете проделать следующий трюк с дизассемблированием, чтобы увидеть, как компилируется конкретный код.

    Vector<int, 16> a, b;
    Vector<int, 65536> c, d;

    asm("xxx"); // marker
    a.Add(b);
    asm("yyy"); // marker
    c.Add(d);
    asm("zzz"); // marker

Теперь скомпилируйте

gcc -O3 1.cc -S -o 1.s

И увидеть дизассем

    xxx
# 0 "" 2
#NO_APP
    movdqa  524248(%rsp), %xmm0
    leaq    524248(%rsp), %rsi
    paddd   524184(%rsp), %xmm0
    movdqa  %xmm0, 524248(%rsp)
    movdqa  524264(%rsp), %xmm0
    paddd   524200(%rsp), %xmm0
    movdqa  %xmm0, 524264(%rsp)
    movdqa  524280(%rsp), %xmm0
    paddd   524216(%rsp), %xmm0
    movdqa  %xmm0, 524280(%rsp)
    movdqa  524296(%rsp), %xmm0
    paddd   524232(%rsp), %xmm0
    movdqa  %xmm0, 524296(%rsp)
#APP
# 36 "1.cc" 1
    yyy
# 0 "" 2
#NO_APP
    leaq    262040(%rsp), %rdx
    leaq    -104(%rsp), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L2:
    movdqa  (%rcx,%rax), %xmm0
    paddd   (%rdx,%rax), %xmm0
    movdqa  %xmm0, (%rdx,%rax)
    addq    $16, %rax
    cmpq    $262144, %rax
    jne .L2
#APP
# 38 "1.cc" 1
    zzz

Как видите, первая петля была достаточно маленькой, чтобы ее можно было развернуть. Второй — петля.

person klimkin    schedule 26.05.2011

Во-первых: современные процессоры довольно умно предсказывают переходы, поэтому развертывание цикла может не помочь (а может даже навредить).

Во-вторых: да, современные компиляторы знают, как развернуть подобный цикл, если это хорошая идея для вашего целевого процессора.

Третье: современные компиляторы могут даже автоматически векторизовать цикл, что даже лучше, чем разворачивание.

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

person Nemo    schedule 26.05.2011

Цикл можно развернуть, используя рекурсивное создание экземпляров шаблона. Это может быть или не быть быстрее в вашей реализации C++.

Я немного подкорректировал ваш пример, чтобы он компилировался.

typedef unsigned u32; // or something similar

template <typename T, u32 size>
class Vector
{
  // need to use an inner class, because member templates of an 
  // unspecialized template cannot be explicitly specialized.
  template<typename Vec, u32 index>
  struct Inner
  {
    static void add(const Vec& a, const Vec& b)
    {
      a.values[index] = b.values[index];
      // triggers recursive instantiation of Inner 
      Inner<Vec, index-1>::add(a,b);
    }
  };
  // this specialization terminates the recursion
  template<typename Vec>
  struct Inner<Vec, 0>
  {
    static void add(const Vec& a, const Vec& b)
    {
      a.values[0] = b.values[0];
    }
  };

public:

    // PS! this function should probably take a 
    // _const_ Vector, because the argument is not modified
    // Various functions for mathematical operations. 
    // The functions take in a Vector<T, size>.
    // Example:
    void add(Vector<T, size>& vec)
    {
      Inner<Vector, size-1>::add(*this, vec);
    }

    T   values[size];
};
person decltype    schedule 26.05.2011

Единственный способ понять это — попробовать на собственном компиляторе с собственными параметрами оптимизации. Создайте один тестовый файл с вашим кодом "разворачивается" test.cpp:

#include "myclass.hpp"

void doSomething(Vector<double, 3>& a, Vector<double, 3>& b) {
    a.add( b );
}

затем фрагмент кода ссылки reference.cpp:

#include "myclass.hpp"

void doSomething(Vector<double, 3>& a, Vector<double, 3>& b) {
    a[0] += b[0];
    a[1] += b[1];
    a[2] += b[2];
}

а теперь используйте GCC, чтобы скомпилировать их и выплюнуть только сборку:

for x in *.cpp; do  g++ -c "$x" -Wall -Wextra -O2 -S -o "out/$x.s"; done

По моему опыту, GCC будет разворачивать циклы из 3 или менее по умолчанию при использовании циклов, продолжительность которых известна во время компиляции; использование -funroll-loops заставит его развернуться еще больше.

person Seth Johnson    schedule 26.05.2011

Во-первых, совсем не факт, что развертывание цикла принесет пользу.

Единственный возможный ответ на ваш вопрос: «это зависит» (от флагов компилятора, от значения size и т. д.).

Если вы действительно хотите знать, спросите у своего компилятора: скомпилируйте в ассемблерный код с типичными значениями size и с флагами оптимизации, которые вы бы использовали на самом деле, и изучите результат.

person NPE    schedule 26.05.2011

Многие компиляторы будут разворачивать этот цикл, не зная, будет ли это делать «компилятор», о котором вы говорите. В мире существует не один компилятор.

Если вы хотите гарантировать, что он развернут, то TMP (с встраиванием) может это сделать. (На самом деле это одно из самых простых применений TMP, часто используемое в качестве примера метапрограммирования).

person Ben Voigt    schedule 26.05.2011
comment
Я добавил соответствующий тег. Это компилятор MSVC. - person Samaursa; 26.05.2011
comment
@Samarusa: компилятора нет. Вы знаете, сколько существует разновидностей MSVC? Добавьте в свой вопрос строку версии (выводимую компилятором при запуске cl.exe /version). - person Ben Voigt; 26.05.2011