В отличие от оптимизированных вручную подходов, представленных в замечательных ответах Вима и Майка, давайте также кратко рассмотрим, что даст нам совершенно ванильная реализация C ++:
std::transform(addon, addon + count, canvas, canvas, std::plus<void>());
Попробуйте здесь . Вы увидите, что даже без каких-либо реальных усилий с вашей стороны, компилятор уже может создавать векторизованный код, который довольно хорош, учитывая, что он не может делать какие-либо предположения относительно выравнивания и размера ваших буферов, а также есть некоторые потенциальные проблемы с наложением ( из-за использования uint8_t
, что, к сожалению, заставляет компилятор предполагать, что указатель может иметь псевдоним для любого другого объекта). Также обратите внимание, что код в основном идентичен тому, что вы получили бы от реализации в стиле C (в зависимости от компилятора, версия C ++ имеет несколько инструкций больше или меньше инструкций)
void f(uint16_t* canvas, const uint8_t* addon, size_t count)
{
for (size_t i = 0; i < count; ++i)
canvas[i] += addon[i];
}
Однако универсальное решение C ++ работает с любой комбинацией различных типов контейнеров и типов элементов, если только можно добавить типы элементов. Таким образом, как также указывалось в других ответах, хотя, безусловно, можно получить чуть более эффективную реализацию от ручной оптимизации, можно проделать долгий путь, просто написав простой код C ++ (если все сделано правильно). Прежде чем прибегать к ручному написанию встроенных функций SSE, подумайте, что универсальное решение C ++ является более гибким, более простым в обслуживании и, особенно, более переносимым. Простым переключением переключателя целевой архитектуры вы можете заставить его генерировать код аналогичного качества не только для SSE, но и для AVX или даже для ARM с NEON и любыми другими наборами команд, которые могут вам понадобиться. Если вам нужно, чтобы ваш код был идеальным вплоть до последней инструкции для одного конкретного варианта использования на одном конкретном процессоре, тогда да, возможно, стоит использовать встроенную или даже встроенную сборку. Но в целом, я бы также предложил вместо этого сосредоточиться на написании своего кода на C ++ таким образом, чтобы компилятор мог и направлял сборку, которую вы хотите, а не генерировал сборку самостоятельно. Например, используя (нестандартный, но общедоступный) ограничивающий квалификатор и заимствуя хитрость, сообщая компилятору, что ваш count
всегда кратен 32
void f(std::uint16_t* __restrict__ canvas, const std::uint8_t* __restrict__ addon, std::size_t count)
{
assert(count % 32 == 0);
count = count & -32;
std::transform(addon, addon + count, canvas, canvas, std::plus<void>());
}
вы получите (-std=c++17 -DNDEBUG -O3 -mavx
)
f(unsigned short*, unsigned char const*, unsigned long):
and rdx, -32
je .LBB0_3
xor eax, eax
.LBB0_2: # =>This Inner Loop Header: Depth=1
vpmovzxbw xmm0, qword ptr [rsi + rax] # xmm0 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
vpmovzxbw xmm1, qword ptr [rsi + rax + 8] # xmm1 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
vpmovzxbw xmm2, qword ptr [rsi + rax + 16] # xmm2 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
vpmovzxbw xmm3, qword ptr [rsi + rax + 24] # xmm3 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
vpaddw xmm0, xmm0, xmmword ptr [rdi + 2*rax]
vpaddw xmm1, xmm1, xmmword ptr [rdi + 2*rax + 16]
vpaddw xmm2, xmm2, xmmword ptr [rdi + 2*rax + 32]
vpaddw xmm3, xmm3, xmmword ptr [rdi + 2*rax + 48]
vmovdqu xmmword ptr [rdi + 2*rax], xmm0
vmovdqu xmmword ptr [rdi + 2*rax + 16], xmm1
vmovdqu xmmword ptr [rdi + 2*rax + 32], xmm2
vmovdqu xmmword ptr [rdi + 2*rax + 48], xmm3
add rax, 32
cmp rdx, rax
jne .LBB0_2
.LBB0_3:
ret
что действительно неплохо ...