Библиотека сайта 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 | 10 | 10 | 10 | вектор1 ------------------------------- ------------------------------- x | 2 | 2 | 2 | 2 | вектор2 ------------------------------- -------------------------------------- ------------------------------- | 20 | 20 | 20 | 20 | вектор3 -------------------------------
Все значения 10 умножаются на значения 2 одновременно.
------------------------------- | 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, вы можете заменить деление в последнем выражении на сдвиг, который в большинстве случаев выполняется быстрее, но более сложен в понимании. (Поэтому не занимайтесь этим необоснованно.)
1.0C + 0.5C + 0.25C + ... или C + (C >> 1) + (C >> 2) + ...
Снова повторимся, что это действие наверняка произойдет быстрее, хотя его и сложно понять.
Примеры операций с целыми числами вы можете найти в файлах samples/simple/temperatures*.c, а пример техники сдвигов и суммирований - в файле samples/colorconv2/scalar.c.
Это только первая часть статьи. Перейти к следующей части.