Как заставить gcc использовать все регистры SSE (или AVX)?

#gcc #64-bit #sse #register-allocation #avx

#gcc #64-разрядный #sse #распределение регистров #avx

Вопрос:

Я пытаюсь написать некоторый код с большими вычислительными затратами для целевой версии Windows x64 с помощью SSE или новых инструкций AVX, компиляции в GCC 4.5.2 и 4.6.1, MinGW64 (сборка TDM GCC и некоторые пользовательские сборки). Мои параметры компилятора -O3 -mavx . ( -m64 подразумевается)

Короче говоря, я хочу выполнить некоторые длительные вычисления для 4 трехмерных векторов упакованных чисел с плавающей точкой. Для этого требуется 4×3 = 12 регистров xmm или ymm для хранения и 2 или 3 регистра для временных результатов. ИМХО, это должно плотно вписываться в 16 доступных регистров SSE (или AVX), доступных для 64-разрядных целей. Однако GCC выдает очень неоптимальный код с переполнением регистров, используя только регистры xmm0-xmm10 и перетасовывая данные из стека в стек. Мой вопрос:

Есть ли способ убедить GCC использовать все регистры xmm0-xmm15 ?

Чтобы исправить идеи, рассмотрим следующий код SSE (только для иллюстрации):

 void example(vect<__m128> q1, vect<__m128> q2, vect<__m128>amp; a1, vect<__m128>amp; a2) {
    for (int i=0; i < 10; i  ) {
        vect<__m128> v = q2 - q1;
        a1  = v;
//      a2 -= v;

        q2 *= _mm_set1_ps(2.);
    }
}
  

Здесь vect<__m128> просто struct из 3 __m128 , с естественным сложением и умножением на скаляр. Когда строка a2 -= v закомментирована, т. Е. нам нужны только регистры 3×3 для хранения, поскольку мы игнорируем a2 , созданный код действительно прост без каких-либо перемещений, все выполняется в регистрах xmm0-xmm10 . Когда я удаляю комментарий a2 -= v , код становится довольно ужасным с большим количеством перетасовок между регистрами и стеком. Хотя компилятор мог бы просто использовать регистры xmm11-xmm13 или что-то в этом роде.

На самом деле я еще не видел, чтобы GCC использовал какой-либо из регистров xmm11-xmm15 где-либо во всем моем коде. Что я делаю не так? Я понимаю, что это регистры, сохраненные вызываемым пользователем, но эти накладные расходы полностью оправданы упрощением кода цикла.

Ответ №1:

Два момента:

  • Во-первых, вы делаете много предположений. Переполнение регистров довольно дешево на процессорах x86 (из-за быстрого кэширования L1, затенения регистров и других хитростей), а доступ только к 64-разрядным регистрам обходится дороже (с точки зрения больших инструкций), так что, возможно, просто версия GCC такая же быстрая, или даже быстрее, чем та, которую вы хотите.
  • Во-вторых, GCC, как и любой компилятор, выделяет регистры наилучшим образом, на что он способен. Нет опции «пожалуйста, сделайте лучше выделение регистров», потому что если бы она была, она всегда была бы включена. Компилятор не пытается вам насолить. (Насколько я помню, распределение регистров является NP-полной проблемой, поэтому компилятор никогда не сможет сгенерировать идеальное решение. Лучшее, что он может сделать, это приблизить)

Итак, если вы хотите улучшить распределение регистров, у вас в основном есть два варианта:

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

Комментарии:

1. На самом деле, есть еще один вариант, о котором я должен был упомянуть: измените свой код, чтобы сделать его более понятным для GCC и его распределителя регистров. Может помочь объявление новых переменных вместо повторного использования старых, а также минимизация области видимости и времени жизни каждой переменной, и просто общие эксперименты взад и вперед могут помочь вам склонить GCC к созданию другого кода

2. Спасибо за быстрый ответ. Вы правы, что я слишком много предполагал. Оказывается, что компилятор выдает желаемый код, если я сохраняю a1 и a2 во временных локальных переменных на время вычисления. По какой-то причине это не было проблемой для компилятора, когда a2 -= v было закомментировано. Не уверен, почему. Что касается длины инструкции, то новая кодировка VEX делает доступ ко всем 16 регистрам эквивалентным.

3.@jalf Похоже, вы не знакомы с современным дизайном компилятора. Вот следующие статьи и презентации по оптимальному распределению регистров O (N) для общего случая:cs.cmu.edu/afs/cs/academic/class/15745-s07/www/papers /… docstoc.com/docs/11350331/…Я понимаю, что раскраска графика может быть сведена к распределению регистров, когда RA ставится как общая проблема с графом. Однако программы не являются обычными графиками и, как таковые, оптимально раскрашиваются в O (N). Это довольно новый результат.

4. @thechao Это не вывод из этого исследования. Вы можете преобразовать любую программу в SSA и выполнить оптимальное распределение регистров в SSA за линейное время, но это не значит, что вы сделали оптимальное распределение регистров для вашей исходной программы. SSA вводит больше переменных, чтобы упростить структуру графа. С такой более простой структурой вы можете оптимально распределять регистры, но это оптимально только для этой более простой структуры с большим количеством переменных; конечный результат не является оптимальным распределением для исходной задачи. Прочитайте презентации, на которые вы ссылались; они говорят именно это!

5. Спасибо вам обоим. @thechao вы правы, я не знал об этих документах. Спасибо, что обратили на них мое внимание. Очень интересный материал. Но, как говорит Брайан, все еще есть несколько предостережений. Тем не менее, я исправляюсь. 🙂

Ответ №2:

На самом деле, то, что вы видите, не является утечкой, это gcc, работающий с a1 и a2 в памяти, потому что он не может знать, имеют ли они псевдонимы. Если вы объявите последние два параметра как vect<__m128>amp; __restrict__ , GCC может и зарегистрирует выделение a1 и a2.