Diferença de desempenho: std :: acumulate vs std :: inner_product vs Loop

Hoje, quero compartilhar algo que me surpreendeu ao tentar implementar esta operação simples:

Encontrei maneiras diferentes de executar a mesma operação:

Usando ostd::inner_product.Implementando um predicado e usando ostd::accumulate função.Usando um loop no estilo C.

Eu queria realizar um benchmark usando o Quick Bench e ativando todas as otimizações.

Antes de tudo, comparei as duas alternativas C ++ com valores flutuantes. Este é o código usado usandostd::accumulate:

const auto predicate = [](const double previous, const double current) {
    return previous + current * current;
};
const auto result = std::accumulate(input.cbegin(), input.cend(), 0, predicate);

Versus esse código usando ostd::inner_product funcionalidade:

const auto result = std::inner_product(input.cbegin(), input.cend(), input.cbegin(), 1);

Depois de executar o benchmark com toda a otimização ativada, obtive este resultado:

Ambos os algoritmos parecem atingir o mesmo desempenho. Eu queria ir mais longe e tentar a implementação C:

double result = 0;
for (auto i = 0; i < input.size(); ++i) {
  result += input[i] * input[i];
}

E surpreendentemente, descobri:

Eu não estava esperando esse resultado. Eu tinha certeza de que havia algo errado, então verifiquei a implementação do GCC:

template<typename _InputIterator1, typename _InputIterator2, typename _Tp>
inline _Tp
inner_product(_InputIterator1 __first1, _InputIterator1 __last1,
      _InputIterator2 __first2, _Tp __init)
{
  // concept requirements
  __glibcxx_function_requires(_InputIteratorConcept<_InputIterator1>)
  __glibcxx_function_requires(_InputIteratorConcept<_InputIterator2>)
  __glibcxx_requires_valid_range(__first1, __last1);

  for (; __first1 != __last1; ++__first1, (void)++__first2)
__init = __init + (*__first1 * *__first2);
  return __init;
}

Eu descobri que estava fazendo o mesmo que a implementação em C. Depois de revisar a implementação, descobri algo estranho (ou pelo menos não esperava ter esse impacto significativo): em todas as acumulações internas, ele fazia uma conversão do iterador value_type para o tipo do valor inicial.

No meu caso, eu estava inicializando os valores iniciais para 0 ou 1, os valores foram considerados inteiros e em cada acumulação, o compilador estava fazendo o casting. Nos diferentes casos de teste, minha matriz de entrada armazena pontos flutuantes truncados, para que o resultado não seja alterado.

Após atualizar o valor inicial para um tipo duplo:

const auto result = std::accumulate(input.cbegin(), input.cend(), 0.0, predicate);

E:

const auto result = std::inner_product(input.cbegin(), input.cend(), input.cbegin(), 0.0);

Eu obtive o resultado esperado:

Agora, entendo que deixar o valor inicial como um tipo independente do tipo subjacente do iterador pode tornar a função mais flexível e permitir fazer mais coisas. Mas,

Se estou acumulando elementos de uma matriz, espero obter o mesmo tipo como resultado. O mesmo para o produto interno.

Deve ser o comportamento padrão?

Por que o padrão decidiu realizá-lo dessa maneira?

questionAnswers(1)

yourAnswerToTheQuestion