Gostaria de obter algumas informações sobre como pensar corretamente sobre encerramentos de C ++ 11 e std::function
em termos de como eles são implementados e como a memória é tratada.
Embora eu não acredite em otimização prematura, tenho o hábito de considerar cuidadosamente o impacto de minhas escolhas no desempenho enquanto escrevo um novo código. Eu também faço uma boa quantidade de programação em tempo real, por exemplo, em microcontroladores e para sistemas de áudio, onde as pausas não determinísticas de alocação / desalocação de memória devem ser evitadas.
Portanto, gostaria de desenvolver um melhor entendimento de quando usar ou não lambdas C ++.
Meu entendimento atual é que um lambda sem encerramento capturado é exatamente como um retorno de chamada C. No entanto, quando o ambiente é capturado por valor ou por referência, um objeto anônimo é criado na pilha. Quando um fechamento de valor deve ser retornado de uma função, ele o envolve std::function
. O que acontece com a memória de fechamento neste caso? É copiado da pilha para a pilha? Ele é liberado sempre que o std::function
é liberado, ou seja, é contado por referência como um std::shared_ptr
?
Imagino que, em um sistema de tempo real, eu pudesse configurar uma cadeia de funções lambda, passando B como um argumento de continuação para A, de modo que um pipeline de processamento A->B
seja criado. Nesse caso, os fechamentos A e B seriam alocados uma vez. Embora eu não tenha certeza se eles seriam alocados na pilha ou no heap. No entanto, em geral, isso parece seguro para uso em um sistema de tempo real. Por outro lado, se B construir alguma função lambda C, que retorna, a memória para C seria alocada e desalocada repetidamente, o que não seria aceitável para uso em tempo real.
Em pseudo-código, um loop DSP, que acho que será seguro em tempo real. Quero realizar o processamento do bloco A e, em seguida, B, onde A chama seu argumento. Ambas as funções retornam std::function
objetos, então f
será um std::function
objeto, onde seu ambiente é armazenado no heap:
auto f = A(B); // A returns a function which calls B
// Memory for the function returned by A is on the heap?
// Note that A and B may maintain a state
// via mutable value-closure!
for (t=0; t<1000; t++) {
y = f(t)
}
E um que eu acho que pode ser ruim para usar em código em tempo real:
for (t=0; t<1000; t++) {
y = A(B)(t);
}
E um onde eu acho que a memória da pilha é provavelmente usada para o fechamento:
freq = 220;
A = 2;
for (t=0; t<1000; t++) {
y = [=](int t){ return sin(t*freq)*A; }
}
No último caso, o encerramento é construído a cada iteração do loop, mas, ao contrário do exemplo anterior, é barato porque é como uma chamada de função, nenhuma alocação de heap é feita. Além disso, eu me pergunto se um compilador poderia "suspender" o encerramento e fazer otimizações in-line.
Isso está correto? Obrigado.
operator()
. Não há "levantamento" a ser feito, lambdas não são nada de especial. Eles são apenas um atalho para um objeto de função local.std::function
armazena seu estado no heap ou não, e não tem nada a ver com lambdas. Isso está certo?std::function
!!auto
tipo de retorno.Respostas:
Não; é sempre um objeto C ++ com um tipo desconhecido, criado na pilha. Um lambda sem captura pode ser convertido em um ponteiro de função (embora seja adequado para as convenções de chamada de C depender da implementação), mas isso não significa que seja um ponteiro de função.
Um lambda não é nada especial em C ++ 11. É um objeto como qualquer outro objeto. Uma expressão lambda resulta em um temporário, que pode ser usado para inicializar uma variável na pilha:
lamb
é um objeto de pilha. Tem um construtor e um destruidor. E seguirá todas as regras do C ++ para isso. O tipo delamb
conterá os valores / referências que são capturados; eles serão membros daquele objeto, assim como quaisquer outros membros de objeto de qualquer outro tipo.Você pode dar a
std::function
:Nesse caso, ele obterá uma cópia do valor de
lamb
. Selamb
tivesse capturado qualquer coisa por valor, haveria duas cópias desses valores; um dentrolamb
e um dentrofunc_lamb
.Quando o escopo atual terminar,
func_lamb
será destruído, seguido porlamb
, de acordo com as regras de limpeza das variáveis da pilha.Você poderia facilmente alocar um na pilha:
Exatamente onde vai a memória para o conteúdo de um
std::function
é dependente da implementação, mas o apagamento de tipo empregado porstd::function
geralmente requer pelo menos uma alocação de memória. É por isso questd::function
o construtor de pode usar um alocador.std::function
armazena uma cópia de seu conteúdo. Como praticamente todo tipo de biblioteca padrão C ++,function
usa semântica de valor . Portanto, é copiável; quando é copiado, o novofunction
objeto é completamente separado. Ele também é móvel, portanto, quaisquer alocações internas podem ser transferidas apropriadamente sem a necessidade de mais alocação e cópia.Portanto, não há necessidade de contagem de referência.
Tudo o mais que você declara está correto, assumindo que "alocação de memória" é igual a "ruim para usar em código em tempo real".
fonte
std::function
é o ponto em que a memória é alocada e copiada. Parece que não há como retornar um fechamento (uma vez que eles estão alocados na pilha), sem primeiro copiar em umstd::function
, sim?std::function
objeto sem memória dinâmica alocação em andamento.function
tem um construtor de movimento noexcept. O objetivo de dizer "geralmente exige" é que não estou dizendo " sempre exige": que há circunstâncias em que nenhuma alocação será realizada.C ++ lambda é apenas um açúcar sintático em torno da classe Functor (anônima) com sobrecarga
operator()
estd::function
é apenas um invólucro em torno de chamáveis (ou seja, functores, lambdas, funções c, ...) que copia por valor o "objeto lambda sólido" do atual escopo da pilha - para a pilha .Para testar o número de construtores / relocatons reais, fiz um teste (usando outro nível de empacotamento para shared_ptr, mas não é o caso). Veja por si mesmo:
faz esta saída:
Exatamente o mesmo conjunto de ctors / dtors seria chamado para o objeto lambda alocado na pilha! (Agora ele chama Ctor para alocação de pilha, Copy-ctor (+ alocação de pilha) para construí-lo em std :: função e outro para fazer alocação de pilha shared_ptr + construção de função)
fonte