Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Внутренние функции компилятора GCC для обработки данных в векторной форме

Оригинал: An Introduction to GCC Compiler Intrinsics in Vector Processing
Авторы: George Koharchik, Kathy Jones
Дата публикации: 21 Сентября 2012 г.
Перевод: А.Панин
Дата перевода: 24 Ноября 2012 г.

Высокая скорость работы очень важна для мультимедийных и графических приложений, а также приложений, осуществляющих обработку сигналов. Иногда разработчики прибегают к использованию языка ассемблера для получения даже минимального повышения скорости работы приложения на их машинах. Компилятор GCC позволяет использовать промежуточный вариант между ассемблером и стандартным языком C, который позволяет повысить скорость работы приложения и использовать специфические возможности центрального процессора, не используя ассемблер: внутренние функции (compiler intrinsics). Эта статья описывает внутренние функции компилятора GCC, при этом выделяются принципы использования этих функций на примере трех платформ: X86 (используются технологии MMX, SSE и SSE2); Motorola, а сейчас Freescale (используется технология Altivec); и ARM Cortex-A (используется технология Neon). В заключении приведены советы по отладке приложений и список материалов для дополнительного ознакомления.

Примечание: Вы можете загрузить исходный код всех рассмотренных в статье примеров по ссылке: http://www.linuxjournal.com/files/linuxjournal.com/code/11108.tar.

Что же такое "внутренние функции компилятора"?

Внутренние функции (иногда они также называются "встроенными функциями" ("builtins")) подобны знакомым вам библиотечным функциям, за тем исключением, что они встроены в компилятор. Они могут быть быстрее обычных библиотечных функций (компилятор знает о них больше, поэтому может лучше оптимизировать) или обрабатывать меньший диапазон исходных данных, нежели библиотечные функции. Внутренние функции также предоставляют доступ к специфическим возможностям процессора, поэтому вы можете использовать их как промежуточное решение между стандартным языком C и языком ассемблера. Это обстоятельство позволяет вам получить функциональность, близкую к ассемблеру, при этом позволяя компилятору выполнять такую работу, как проверка типов, распределение регистров, установление порядка следования команд и обслуживание стека вызовов. Некоторые функции являются переносимыми, некоторые не являются таковыми - они зависят от конкретного процессора. Вы можете найти списки переносимых и зависящих от оборудования функций на страницах руководства GCC и в заголовочных файлах (об этом будет написано ниже). Центральной темой этой статьи являются функции для обработки векторов данных.

Векторы и скаляры

В данной статье под векторами понимается упорядоченный набор значений, как массив. Если все элементы вектора являются значениями одного контекста, вектор называется однородным. Неоднородные векторы состоят из элементов, относящихся к разным контекстам и их элементы должны обрабатываться по-разному. В области программного обеспечения векторы имеют свои собственные типы и операции. Скаляр является отдельным значением, вектором длиной в единицу. Код, использующий векторные типы и операции, называется кодом обработки векторов. Код, использующий только скалярные типы и операции, называется кодом обработки скаляров.

Принципы обработки векторов

Операции по обработке векторов относятся к категории обработки одиночным потоком команд множественных потоков данных (Single Instruction, Multiple Data (SIMD)). В SIMD одна операция применяется ко всем данным (значениям вектора) одновременно. Каждое значение вектора рассчитывается независимо от других. Операции над векторами включают в себя логические и математические операции. Математические операции в рамках одного вектора называются горизонтальными математическими операциями. Математические операции между двумя векторами называются вертикальными математическими операциями.

Вместо записи 10 x 2 = 20, запишем это выражение вертикально в следующей форме:
                          10
                        x  2
                       ------
                          20
В вертикальных математических операциях векторы являются массивами, содержащими эти значения; множество операций выполняется одновременно:
        -------------------------------
        |  10   |   10  |  10  |  10  |      вектор1
        -------------------------------
        -------------------------------
    x   |  2    |   2   |  2   |  2   |      вектор2
        -------------------------------
   --------------------------------------
        -------------------------------
        |  20   |   20  |  20  |  20  |      вектор3
        -------------------------------

Все значения 10 умножаются на значения 2 одновременно.

Таким образом, перевод значений температур из градусов Цельсия в градусы Фаренгейта по формуле F = (9/5) * C + 32 для заданного вектора температур в градусах Цельсия, выглядит следующим образом:
       -------------------------------
        |  C0   |   C1  |  C2  |  C3  |    вектор температур в градусах Цельсия
        -------------------------------
        -------------------------------
    x   |  9    |   9   |  9   |  9   |    вектор2
        -------------------------------
   --------------------------------------
        -------------------------------
        |  p1   |   p2  |  p3  |  p4  |    промежуточный результат
        -------------------------------
        -------------------------------
    /   |  5    |   5   |  5   |  5   |    вектор3
        -------------------------------
   --------------------------------------
        -------------------------------
        |  p1   |   p2  |  p3  |  p4  |    промежуточный результат
        -------------------------------
        -------------------------------
   +    |  32   |   32  |  32  |  32  |    вектор4
        -------------------------------
    --------------------------------------

        -------------------------------
        |  F0   |   F1  |  F2  |  F3  |    вектор температур в градусах Фаренгейта
        -------------------------------

Арифметика насыщения (saturation arithmetic) не отличается от обычной арифметики за тем исключением, что в тех случаях, когда значение результата операции становится больше максимального или меньше минимального значения типа элемента вектора, в качестве результата устанавливается крайнее значение диапазона, переход за которое невозможен. (Например, 255 является максимальным значением для типа беззнакового символа (unsigned character). В арифметике насыщения с применением беззнаковых символов 250 + 10 = 255). Обычная арифметика позволяет преодолеть нулевое значение и в итоге получить меньшее значение. Например, арифметика насыщения полезна в том случае, когда яркость пикселей изображения должна быть немного повышена. При увеличении яркости должен быть предел значений, переход через который в обычной арифметике мог бы привести к переполнению и сделать изображение темнее, что нежелательно.

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

Первым вариантом перехода к работе с целыми числами является перераспределение действий. Если ваша формула достаточно проста, у вас должна быть возможность для перераспределения действий для сохранения точности. Например, вы можете произвести следующее перераспределение из формы:
F = (9/5)C + 32
в форму:
F = (9*C)/5 + 32

Поскольку выражение 9*C не приводит к переполнению используемого типа, точность сохраняется. Она теряется при делении на 5; поэтому это действие производится после умножения. Перераспределение может не работать в случае более сложных формул.

Следующим вариантом перехода к работе с целыми числами является масштабирование значений. В этом случае вы решаете, какая точность вам необходима, после чего умножаете обе части вашего выражения на константу, округляете или отбрасываете дробную часть ваших коэффициентов, приводя их к целочисленному значению, и работаете с ними. Например, если вы хотите перевести градусы Цельсия в градусы Фаренгейта:
F = (9/5)C + 32
  = 1.8C + 32            -- мы не можем работать со значением 1.8, поэтому умножаем на 10 
sum = 10F = 18C + 320    -- 1.8 сейчас 18: все операции с целыми числами
F = sum/10

Если вы умножаете на число, являющееся степенью 2 вместо 10, вы можете заменить деление в последнем выражении на сдвиг, который в большинстве случаев выполняется быстрее, но более сложен в понимании. (Поэтому не занимайтесь этим необоснованно.)

Третьим вариантом перехода к работе с целыми числами является техника сдвигов и суммирований (shift-and-add). Эта техника также базируется на идее, что умножение чисел с плавающей точкой может быть реализовано при помощи множества сдвигов и суммирований результатов. Таким образом, наше проблемное выражение 1.8*C может быть аппроксимировано следующим образом:
1.0C + 0.5C + 0.25C + ...   или  C + (C >> 1) + (C >> 2) + ...

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

Примеры операций с целыми числами вы можете найти в файлах samples/simple/temperatures*.c, а пример техники сдвигов и суммирований - в файле samples/colorconv2/scalar.c.


Это только первая часть статьи. Перейти к следующей части.