Во многих случаях этап оптимизации компилятора позаботится об этом за вас. Для примера приведем определение этой функции
const X: [i32; 8] = [0, 1, -1, 0, 0, 1, 0, -1];
pub fn dot_x(y: [i32; 8]) -> i32 {
X.iter().zip(y.iter()).map(|(i, j)| i * j).sum()
}
приводит к выводу этой сборки на x86_64:
playground::dot_x:
mov eax, dword ptr [rdi + 4]
sub eax, dword ptr [rdi + 8]
add eax, dword ptr [rdi + 20]
sub eax, dword ptr [rdi + 28]
ret
Вы не сможете получить более оптимизированную версию, чем эта, поэтому простое написание кода - лучшее решение. Неясно, будет ли компилятор развернуть цикл для более длинных векторов, и это может измениться с версиями компилятора.
Для чисел с плавающей запятой компилятор, как правило, не может выполнить все описанные выше оптимизации, поскольку не гарантируется, что числа в y
конечны - они также могут быть NaN
, inf
или -inf
. По этой причине умножение на 0.0
не обязательно снова приведет к 0.0
, поэтому компилятору необходимо сохранить инструкции умножения в коде. Вы можете явно разрешить ему предполагать, что все числа конечны, используя встроенную функцию fmul_fast()
:
#![feature(core_intrinsics)]
use std::intrinsics::fmul_fast;
const X: [i32; 8] = [0, 1, -1, 0, 0, 1, 0, -1];
pub fn dot_x(y: [f64; 8]) -> f64 {
X.iter().zip(y.iter()).map(|(i, j)| unsafe { fmul_fast(*i as f64, *j) }).sum()
}
В результате получается следующий код сборки:
playground::dot_x: # @playground::dot_x
# %bb.0:
xorpd xmm1, xmm1
movsd xmm0, qword ptr [rdi + 8] # xmm0 = mem[0],zero
addsd xmm0, xmm1
subsd xmm0, qword ptr [rdi + 16]
addsd xmm0, xmm1
addsd xmm0, qword ptr [rdi + 40]
addsd xmm0, xmm1
subsd xmm0, qword ptr [rdi + 56]
ret
Это все еще избыточно добавляет нули между шагами, но я не ожидаю, что это приведет к каким-либо измеримым накладным расходам для реалистичного моделирования CFD, так как такие моделирования, как правило, ограничиваются пропускной способностью памяти, а не процессором. Если вы также хотите избежать этих дополнений, вам нужно использовать fadd_fast()
для дополнений, чтобы компилятор мог оптимизировать дальше:
#![feature(core_intrinsics)]
use std::intrinsics::{fadd_fast, fmul_fast};
const X: [i32; 8] = [0, 1, -1, 0, 0, 1, 0, -1];
pub fn dot_x(y: [f64; 8]) -> f64 {
let mut result = 0.0;
for (&i, &j) in X.iter().zip(y.iter()) {
unsafe { result = fadd_fast(result, fmul_fast(i as f64, j)); }
}
result
}
В результате получается следующий код сборки:
playground::dot_x: # @playground::dot_x
# %bb.0:
movsd xmm0, qword ptr [rdi + 8] # xmm0 = mem[0],zero
subsd xmm0, qword ptr [rdi + 16]
addsd xmm0, qword ptr [rdi + 40]
subsd xmm0, qword ptr [rdi + 56]
ret
Как и во всех вариантах, вы должны начать с самой читаемой и поддерживаемой версии кода. Если производительность становится проблемой, вы должны профилировать свой код и найти узкие места. В качестве следующего шага попытайтесь улучшить фундаментальный подход, например, используя алгоритм с лучшей асимптотической сложностью. Только тогда вы должны обратиться к микрооптимизации, подобной той, которую вы предложили в вопросе.