В обсуждении упоминалось, что оптимальным решением может быть
конкретная архитектура Кто-то также предложил закодировать его в сборке.
Сборка имеет стоимость с точки зрения мобильности, но она также просит
вопрос о том (можно ли и сколько) побить компилятор
оптимизатор.
Я провел эксперимент на Arduino, который основан на AVR
микроконтроллер. Это очень ограниченный 8-битный, Гарвардский, RISC MCU, с
аппаратный множитель 8 × 8 → 16 бит.
Вот простая реализация, использующая типизацию для
умножить отдельные байты:
static inline uint32_t scale_pixel(uint32_t rgbw, uint16_t scale)
{
union {
uint32_t value;
uint8_t bytes[4];
} x = { .value = rgbw };
x.bytes[0] = x.bytes[0] * scale >> 8;
x.bytes[1] = x.bytes[1] * scale >> 8;
x.bytes[2] = x.bytes[2] * scale >> 8;
x.bytes[3] = x.bytes[3] * scale >> 8;
return x.value;
}
Скомпилировано с gcc на -Os
(типично для этих устройств с ограниченным объемом памяти)
для этого требуется 28 циклов ЦП, то есть 7 циклов на байт.
Компилятор достаточно умен, чтобы выделить rgbw
и x
для одного и того же процессора
регистрируется и, таким образом, избегает копирования.
Вот версия, основанная на ответе Гарольда:
static inline uint32_t scale_pixel(uint32_t rgbw, uint16_t scale)
{
uint32_t rb = rgbw & 0x00FF00FF;
uint32_t gw = (rgbw >> 8) & 0x00FF00FF;
rb *= scale;
gw *= scale;
uint32_t out = ((rb >> 8) & 0x00FF00FF) | (gw & 0xFF00FF00);
return out;
}
Это очень умная оптимизация, которая может окупиться на 32-битной
MCU. Однако на этом маленьком 8-битном процессоре потребовалось 176 циклов
выполнить! Сгенерированная сборка имеет два вызова библиотечной функции
который реализует полное 32-битное умножение, наряду со многими движущимися и
очистка регистров.
Наконец, вот моя встроенная версия сборки:
static inline uint32_t scale_pixel(uint32_t rgbw, uint16_t scale)
{
asm(
"tst %B[scale] \n\t" // test high byte of scale
"brne 0f \n\t" // if non zero, we are done
"mul %A[rgbw], %A[scale] \n\t" // multiply LSB
"mov %A[rgbw], r1 \n\t" // move result into place
"mul %B[rgbw], %A[scale] \n\t" // same with three other bytes
"mov %B[rgbw], r1 \n\t" // ...
"mul %C[rgbw], %A[scale] \n\t"
"mov %C[rgbw], r1 \n\t"
"mul %D[rgbw], %A[scale] \n\t"
"mov %D[rgbw], r1 \n"
"0:"
: [rgbw] "+r" (rgbw) // output
: [scale] "r" (scale) // input
: "r0", "r1" // clobbers
);
return rgbw;
}
Этот использует тот факт, что масштабный коэффициент не может быть больше 256.
Фактически, любой фактор больше 256 рассматривается как 256, что может быть
считается особенностью. Выполнение занимает 14 циклов, и только
3 цикла, если шкала 256.
Резюме:
- 176 циклов для версии, оптимизированной для 32-битного ядра
- 28 циклов для версии с наивным типом штамповки
- 14 циклов для версии сборки
Мой вывод из этого эксперимента заключается в том, что вы смотрите здесь на
вид микрооптимизации, где архитектура действительно имеет значение. Ты не можешь
серьезно попытаться оптимизировать это на уровне C без каких-либо предположений
об архитектуре, на которой он будет работать. Также, если фактор 2 в скорости
имеет значение для вас, стоит попробовать реализацию в сборке. использование
условная компиляция, чтобы включить реализацию asm в
целевой архитектуры, и вернуться к общей реализации C в
любая другая архитектура.