Graças ao C ++ 11, recebemos a std::function
família de wrappers functor. Infelizmente, continuo ouvindo apenas coisas ruins sobre essas novas adições. O mais popular é que eles são terrivelmente lentos. Eu testei e eles realmente são ruins em comparação com os modelos.
#include <iostream>
#include <functional>
#include <string>
#include <chrono>
template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }
float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }
int main() {
using namespace std::chrono;
const auto tp1 = system_clock::now();
for (int i = 0; i < 1e8; ++i) {
calc1([](float arg){ return arg * 0.5f; });
}
const auto tp2 = high_resolution_clock::now();
const auto d = duration_cast<milliseconds>(tp2 - tp1);
std::cout << d.count() << std::endl;
return 0;
}
111 ms vs 1241 ms. Suponho que isso ocorre porque os modelos podem ser bem alinhados, enquanto function
s cobrem os internos por meio de chamadas virtuais.
Obviamente, os modelos têm seus problemas como eu os vejo:
- eles precisam ser fornecidos como cabeçalhos, o que não é algo que você não queira fazer ao liberar sua biblioteca como um código fechado,
- eles podem levar o tempo de compilação muito mais longo, a menos que uma
extern template
política semelhante seja introduzida, - não existe (pelo menos para mim) uma maneira limpa de representar requisitos (conceitos, alguém?) de um modelo, exceto um comentário descrevendo que tipo de função é esperado.
Assim, posso assumir que function
s pode ser usado como padrão de fato de passagem de functores e em locais onde se espera que modelos de alto desempenho sejam usados?
Editar:
Meu compilador é o Visual Studio 2012 sem CTP.
c++
templates
c++11
std-function
XIII vermelho
fonte
fonte
std::function
se e somente se você realmente precisar de uma coleção heterogênea de objetos que podem ser chamados (ou seja, nenhuma informação discriminante adicional está disponível no tempo de execução).std::function
ou modelos". Acho que aqui a questão é simplesmente envolver um lambda em vez destd::function
não envolver um lambdastd::function
. No momento, sua pergunta é como perguntar "devo preferir uma maçã ou uma tigela?"Respostas:
Em geral, se você estiver enfrentando uma situação de design que lhe permita uma escolha, use modelos . Eu enfatizei a palavra design porque acho que o que você precisa focar é a distinção entre os casos de uso
std::function
e os modelos, que são bem diferentes.Em geral, a escolha de modelos é apenas uma instância de um princípio mais amplo: tente especificar o máximo de restrições possível em tempo de compilação . A lógica é simples: se você conseguir detectar um erro ou uma incompatibilidade de tipo, mesmo antes da geração do seu programa, não enviará um programa de buggy ao seu cliente.
Além disso, como você apontou corretamente, as chamadas para funções de modelo são resolvidas estaticamente (ou seja, em tempo de compilação), de modo que o compilador possui todas as informações necessárias para otimizar e possivelmente incorporar o código (o que não seria possível se a chamada fosse executada através de um tabela).
Sim, é verdade que o suporte ao modelo não é perfeito e o C ++ 11 ainda não possui suporte para conceitos; no entanto, não vejo como
std::function
isso o salvaria a esse respeito.std::function
não é uma alternativa aos modelos, mas uma ferramenta para situações de design em que os modelos não podem ser usados.Um desses casos de uso surge quando você precisa resolver uma chamada em tempo de execução , invocando um objeto que pode ser chamado que adere a uma assinatura específica, mas cujo tipo concreto é desconhecido no momento da compilação. Normalmente, esse é o caso quando você tem uma coleção de retornos de chamada de tipos potencialmente diferentes , mas que você precisa chamar de maneira uniforme ; o tipo e o número dos retornos de chamada registrados são determinados em tempo de execução com base no estado do seu programa e na lógica do aplicativo. Alguns desses retornos de chamada podem ser functors, alguns podem ser funções simples, outros podem ser o resultado de vincular outras funções a determinados argumentos.
std::function
estd::bind
também oferecem um idioma natural para ativar a programação funcional em C ++, onde as funções são tratadas como objetos e são naturalmente curry e combinadas para gerar outras funções. Embora esse tipo de combinação também possa ser alcançado com os modelos, uma situação de design semelhante normalmente vem junto com casos de uso que exigem determinar o tipo de objetos que podem ser combinados em tempo de execução.Finalmente, há outras situações em que
std::function
é inevitável, por exemplo, se você deseja escrever lambdas recursivas ; no entanto, essas restrições são mais ditadas por limitações tecnológicas do que por distinções conceituais que acredito.Para resumir, concentre-se no design e tente entender quais são os casos de uso conceitual para essas duas construções. Se você as compara da maneira que você fez, você as está forçando a uma arena à qual provavelmente não pertencem.
fonte
std::function
no final do armazenamento eFun
no modelo na interface".unique_ptr<void>
chamar destruidores apropriados mesmo para tipos sem destruidores virtuais).Andy Prowl cobriu bem questões de design. Isso é, obviamente, muito importante, mas acredito que a pergunta original se refere a mais problemas de desempenho relacionados a
std::function
.Primeiro, uma observação rápida sobre a técnica de medição: os 11ms obtidos para
calc1
não têm nenhum significado. De fato, olhando para o assembly gerado (ou depurando o código do assembly), pode-se ver que o otimizador do VS2012 é inteligente o suficiente para perceber que o resultado da chamadacalc1
é independente da iteração e move a chamada para fora do loop:Além disso, percebe que a chamada
calc1
não tem efeito visível e descarta a chamada completamente. Portanto, os 111ms são o tempo que o loop vazio leva para executar. (Estou surpreso que o otimizador tenha mantido o loop.) Portanto, tenha cuidado com as medições de tempo em loops. Isso não é tão simples quanto parece.Como foi apontado, o otimizador tem mais problemas para entender
std::function
e não move a chamada para fora do loop. Portanto, 1241ms é uma medida justa paracalc2
.Observe que
std::function
é capaz de armazenar diferentes tipos de objetos que podem ser chamados. Portanto, ele deve executar alguma mágica de apagamento de tipo para o armazenamento. Geralmente, isso implica em uma alocação dinâmica de memória (por padrão, através de uma chamada paranew
). É sabido que esta é uma operação bastante cara.O padrão (20.8.11.2.1 / 5) incorpora implementações para evitar a alocação dinâmica de memória para objetos pequenos que, felizmente, o VS2012 faz (em particular, para o código original).
Para ter uma idéia de quanto pode ser mais lento quando a alocação de memória está envolvida, alterei a expressão lambda para capturar três
float
s. Isso torna o objeto que pode ser chamado muito grande para aplicar a otimização de objetos pequenos:Para esta versão, o tempo é de aproximadamente 16000ms (comparado a 1241ms para o código original).
Por fim, observe que o tempo de vida do lambda encerra o do
std::function
. Nesse caso, em vez de armazenar uma cópia do lambda,std::function
poderia armazenar uma "referência" a ele. Por "referência" quero dizer umstd::reference_wrapper
que é facilmente construído por funçõesstd::ref
estd::cref
. Mais precisamente, usando:o tempo diminui para aproximadamente 1860ms.
Eu escrevi sobre isso há um tempo atrás:
http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059
Como eu disse no artigo, os argumentos não se aplicam ao VS2010 devido ao seu fraco suporte ao C ++ 11. No momento da redação deste artigo, apenas uma versão beta do VS2012 estava disponível, mas seu suporte ao C ++ 11 já era bom o suficiente para esse assunto.
fonte
calc1
poderia levar umfloat
argumento que seria o resultado da iteração anterior. Algo comox = calc1(x, [](float arg){ return arg * 0.5f; });
. Além disso, devemos garantir que oscalc1
usosx
. Mas isso ainda não é suficiente. Precisamos criar um efeito colateral. Por exemplo, após a medição, imprimax
na tela. Mesmo assim, eu concordo que o uso de códigos de brinquedo para medições de tempo nem sempre pode dar uma indicação perfeita do que acontecerá com o código real / de produção.std::reference_wrapper
(coagir modelos; não é apenas para armazenamento geral), e é engraçado ver o otimizador do VS falhando em descartar um loop vazio ... como notei com este bug do GCCvolatile
.Com Clang, não há diferença de desempenho entre os dois
Usando clang (3.2, tronco 166872) (-O2 no Linux), os binários dos dois casos são realmente idênticos .
-Vou voltar a tocar no final do post. Mas primeiro, gcc 4.7.2:
Já existe muita percepção, mas quero ressaltar que o resultado dos cálculos de calc1 e calc2 não é o mesmo, devido ao alinhamento interno etc. Compare, por exemplo, a soma de todos os resultados:
com calc2 que se torna
enquanto com calc1, torna-se
isso é um fator de ~ 40 na diferença de velocidade e um fator de ~ 4 nos valores. A primeira é uma diferença muito maior do que o OP postado (usando o visual studio). Na verdade, imprimir o valor no final também é uma boa idéia para impedir que o compilador remova o código sem resultado visível (regra como se). Cassio Neri já disse isso em sua resposta. Observe como os resultados são diferentes - Deve-se ter cuidado ao comparar fatores de velocidade de códigos que executam cálculos diferentes.
Além disso, para ser justo, comparar várias maneiras de calcular repetidamente f (3.3) talvez não seja tão interessante. Se a entrada for constante, ela não deve estar em loop. (É fácil para o otimizador perceber)
Se eu adicionar um argumento de valor fornecido pelo usuário a calc1 e 2, o fator de velocidade entre calc1 e calc2 se reduz a um fator de 5, a partir de 40! No visual studio, a diferença é próxima de um fator 2, e no clang, não há diferença (veja abaixo).
Além disso, como as multiplicações são rápidas, falar sobre fatores de desaceleração geralmente não é tão interessante. Uma pergunta mais interessante é: quão pequenas são suas funções e essas são chamadas de gargalo em um programa real?
Clang:
Clang (usei 3.2) produziu binários idênticos quando alterno entre calc1 e calc2 para o código de exemplo (publicado abaixo). Com o exemplo original publicado na pergunta, ambos também são idênticos, mas não demoram muito (os loops são completamente removidos conforme descrito acima). Com o meu exemplo modificado, com -O2:
Número de segundos para executar (melhor de 3):
Os resultados calculados de todos os binários são os mesmos e todos os testes foram executados na mesma máquina. Seria interessante se alguém com um conhecimento mais profundo do clang ou do VS pudesse comentar sobre quais otimizações podem ter sido feitas.
Meu código de teste modificado:
Atualizar:
Adicionado vs2015. Notei também que existem conversões double-> float em calc1, calc2. Removê-los não altera a conclusão do visual studio (ambos são muito mais rápidos, mas a proporção é a mesma).
fonte
Diferente não é o mesmo.
É mais lento porque faz coisas que um modelo não pode fazer. Em particular, ele permite chamar qualquer função que possa ser chamada com os tipos de argumento fornecidos e cujo tipo de retorno seja conversível no mesmo tipo de retorno especificado .
Observe que o mesmo objeto de função,,
fun
está sendo passado para as duas chamadas paraeval
. Possui duas funções diferentes .Se você não precisa fazer isso, não deve usar
std::function
.fonte
Você já tem boas respostas aqui, então não vou contradizê-las. Em resumo, comparar std :: function com templates é como comparar funções virtuais com funções. Você nunca deve "preferir" funções virtuais a funções, mas usa funções virtuais quando isso se encaixa no problema, movendo decisões do tempo de compilação para o tempo de execução. A idéia é que, em vez de ter que resolver o problema usando uma solução sob medida (como uma tabela de salto), você use algo que dê ao compilador uma melhor chance de otimizar para você. Também ajuda outros programadores, se você usar uma solução padrão.
fonte
Esta resposta pretende contribuir, para o conjunto de respostas existentes, o que eu acredito ser uma referência mais significativa para o custo de tempo de execução das chamadas std :: function.
O mecanismo std :: function deve ser reconhecido pelo que fornece: Qualquer entidade que possa ser chamada pode ser convertida em uma função std :: de assinatura apropriada. Suponha que você tenha uma biblioteca que ajuste uma superfície a uma função definida por z = f (x, y), você pode escrevê-la para aceitar a
std::function<double(double,double)>
e o usuário da biblioteca pode converter facilmente qualquer entidade que possa ser chamada; seja uma função comum, um método de uma instância de classe ou um lambda, ou qualquer coisa suportada pelo std :: bind.Diferentemente das abordagens de modelo, isso funciona sem a necessidade de recompilar a função de biblioteca para diferentes casos; consequentemente, pouco código compilado extra é necessário para cada caso adicional. Sempre foi possível fazer isso acontecer, mas costumava exigir alguns mecanismos estranhos, e o usuário da biblioteca provavelmente precisaria construir um adaptador em torno de sua função para fazê-la funcionar. A função std :: constrói automaticamente qualquer adaptador necessário para obter uma interface de chamada de tempo de execução comum para todos os casos, que é um recurso novo e muito poderoso.
Na minha opinião, este é o caso de uso mais importante para std :: function no que diz respeito ao desempenho: estou interessado no custo de chamar uma função std :: muitas vezes depois de ter sido construída uma vez e precisa pode ser uma situação em que o compilador não consegue otimizar a chamada conhecendo a função que está sendo chamada (ou seja, você precisa ocultar a implementação em outro arquivo de origem para obter uma referência adequada).
Fiz o teste abaixo, semelhante ao OP; mas as principais mudanças são:
Os resultados obtidos são:
case (a) (inline) 1,3 nsec
todos os outros casos: 3,3 nsec.
O caso (d) tende a ser um pouco mais lento, mas a diferença (cerca de 0,05 ns) é absorvida pelo ruído.
A conclusão é que a função std :: é uma sobrecarga comparável (no momento da chamada) ao uso de um ponteiro de função, mesmo quando há uma adaptação simples de 'ligação' à função real. O inline é 2 ns mais rápido que os outros, mas é uma troca esperada, pois o inline é o único caso que é 'conectado por fio' no tempo de execução.
Quando executo o código de johan-lundberg na mesma máquina, vejo cerca de 39 nsec por loop, mas há muito mais no loop, incluindo o construtor e destruidor da função std ::, que provavelmente é bastante alta uma vez que envolve um novo e excluir.
-O2 gcc 4.8.1, para o destino x86_64 (core i5).
Observe que o código é dividido em dois arquivos, para impedir que o compilador expanda as funções onde são chamadas (exceto no caso em que se destina).
----- primeiro arquivo de origem --------------
----- segundo arquivo de origem -------------
Para os interessados, aqui está o adaptador que o compilador construiu para fazer 'mul_by' parecer um float (float) - isso é 'chamado' quando a função criada como bind (mul_by, _1,0.5) é chamada:
(portanto, poderia ter sido um pouco mais rápido se eu tivesse escrito 0,5f no bind ...) Observe que o parâmetro 'x' chega em% xmm0 e permanece lá.
Aqui está o código na área em que a função é construída, antes de chamar test_stdfunc - execute o c ++ filt:
fonte
Achei seus resultados muito interessantes, então eu procurei um pouco para entender o que está acontecendo. Primeiro, como muitos outros disseram, sem ter os resultados do efeito computacional do estado do programa, o compilador apenas otimizará isso. Em segundo lugar, tendo um 3.3 constante como armamento para o retorno de chamada, suspeito que haverá outras otimizações em andamento. Com isso em mente, alterei um pouco o código de referência.
Dada essa alteração no código, compilei com o gcc 4.8 -O3 e obtive um tempo de 330ms para o calc1 e 2702 para o calc2. Portanto, o uso do modelo foi 8 vezes mais rápido, esse número pareceu suspeito, a velocidade de uma potência de 8 geralmente indica que o compilador vetorizou algo. quando olhei o código gerado para a versão dos modelos, ele estava claramente vectoreizado
Onde, como a versão std :: function não estava. Isso faz sentido para mim, uma vez que, com o modelo, o compilador sabe com certeza que a função nunca será alterada ao longo do loop, mas com a função std :: passada, ela poderá mudar, portanto, não poderá ser vetorizada.
Isso me levou a tentar outra coisa para ver se eu poderia fazer o compilador executar a mesma otimização na versão std :: function. Em vez de passar uma função, eu criei uma função std :: como uma var global e chamei isso.
Com esta versão, vemos que o compilador agora vetorizou o código da mesma maneira e obtive os mesmos resultados de benchmark.
Portanto, minha conclusão é que a velocidade bruta de uma função std :: vs uma função de template é praticamente a mesma. No entanto, torna o trabalho do otimizador muito mais difícil.
fonte
calc3
caso não faz sentido; Agora o calc3 está codificado para chamar f2. Claro que isso pode ser otimizado.