Por que meu código fica mais lento quando removo verificações de limites?
Estou escrevendo uma biblioteca de álgebra linear em Rust.
Eu tenho uma função para obter uma referência a uma célula de matriz em uma determinada linha e coluna. Essa função começa com um par de asserções de que a linha e a coluna estão dentro dos limites:
#[inline(always)]
pub fn get(&self, row: usize, col: usize) -> &T {
assert!(col < self.num_cols.as_nat());
assert!(row < self.num_rows.as_nat());
unsafe {
self.get_unchecked(row, col)
}
}
Em laços apertados, pensei que poderia ser mais rápido ignorar a verificação de limites, por isso forneço umaget_unchecked
método:
#[inline(always)]
pub unsafe fn get_unchecked(&self, row: usize, col: usize) -> &T {
self.data.get_unchecked(self.row_col_index(row, col))
}
O mais estranho é que, quando eu uso esses métodos para implementar a multiplicação de matrizes (por meio de iteradores de linha e coluna), meus benchmarks mostram que na verdade é 33% mais rápido quando euVerifica os limites. Por que isso está acontecendo?
Eu tentei isso em dois computadores diferentes, um executando Linux e outro OSX, e ambos mostram o efeito.
O código completo éno github. O arquivo relevante élib.rs. Funções de interesse são:
get
na linha 68get_unchecked
na linha 81next
na linha 551mul
na linha 796matrix_mul
(referência) na linha 1038Observe que estou usando números de nível de tipo para parametrizar minhas matrizes (com a opção de tamanhos dinâmicos também por meio de tipos com tags simuladas), portanto, o benchmark está multiplicando duas matrizes 100x100.
ATUALIZAR:
Simplifiquei significativamente o código, removendo itens não usados diretamente no benchmark e removendo parâmetros genéricos. Também escrevi uma implementação de multiplicação sem usar iteradores, e essa versãonão causa o mesmo efeito. Vejoaqui para esta versão do código. Clonando ominimal-performance
ramificação e execuçãocargo bench
fará o benchmark das duas implementações diferentes de multiplicação (observe que as afirmações são comentadas para começar nesse ramo).
Também digno de nota é que, se eu mudar oget*
para retornar cópias dos dados em vez de referências (f64
ao invés de&f64
), o efeito desaparece (mas o código é um pouco mais lento o tempo todo).