¿Por qué el compilador puede optimizar mejor las lambdas que las funciones simples?

171

En su libro, The C++ Standard Library (Second Edition)Nicolai Josuttis afirma que el compilador puede optimizar mejor las lambdas que las funciones simples.

Además, los compiladores de C ++ optimizan las lambdas mejor que las funciones normales. (Página 213)

¿Porqué es eso?

Pensé que cuando se trata de alinear ya no debería haber ninguna diferencia. La única razón por la que podría pensar es que los compiladores pueden tener un mejor contexto local con lambdas y eso puede hacer más suposiciones y realizar más optimizaciones.

Stephan Dollberg
fuente
Relacionados .
iammilind
Básicamente, la declaración se aplica a todos los objetos de función , no solo a lambdas.
newacct
44
Eso sería incorrecto porque los punteros de función también son objetos de función.
Johannes Schaub - litb
2
@litb: Creo que no estoy de acuerdo con eso. ^ W ^ W ^ W ^ W ^ W ^ W (después de analizar el estándar) No estaba al tanto de ese isma C ++, aunque creo que en términos comunes (y de acuerdo con wikipedia), las personas se refieren a una instancia de clase invocable cuando dicen objeto de función.
Sebastian Mach
1
Algunos compiladores pueden optimizar mejor las lambdas que las funciones simples, pero no todas :-(
Cody Gray

Respuestas:

175

La razón es que las lambdas son objetos de función, por lo que pasarlas a una plantilla de función creará una nueva función específicamente para ese objeto. De este modo, el compilador puede en línea trivialmente la llamada lambda.

Para las funciones, por otro lado, se aplica la advertencia anterior: se pasa un puntero de función a la plantilla de función, y los compiladores tradicionalmente tienen muchos problemas para alinear llamadas a través de punteros de función. Teóricamente pueden estar en línea, pero solo si la función circundante también está en línea.

Como ejemplo, considere la siguiente plantilla de función:

template <typename Iter, typename F>
void map(Iter begin, Iter end, F f) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

Llamándolo con una lambda como esta:

int a[] = { 1, 2, 3, 4 };
map(begin(a), end(a), [](int n) { return n * 2; });

Resultados en esta instanciación (creada por el compilador):

template <>
void map<int*, _some_lambda_type>(int* begin, int* end, _some_lambda_type f) {
    for (; begin != end; ++begin)
        *begin = f.operator()(*begin);
}

... el compilador sabe _some_lambda_type::operator ()y puede hacer llamadas en línea de manera trivial. (E invocar la función mapcon cualquier otra lambda crearía una nueva instancia de mapya que cada lambda tiene un tipo distinto).

Pero cuando se llama con un puntero de función, la instanciación se ve de la siguiente manera:

template <>
void map<int*, int (*)(int)>(int* begin, int* end, int (*f)(int)) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

… Y aquí fapunta a una dirección diferente para cada llamada mapy, por lo tanto, el compilador no puede hacer llamadas en línea a fmenos que la llamada a maptambién se haya en línea para que el compilador pueda resolver funa función específica.

Konrad Rudolph
fuente
44
Quizás valga la pena mencionar que crear una instancia de la misma plantilla de función con una expresión lambda diferente creará una función completamente nueva con un tipo único, que bien podría ser un inconveniente.
relajarse
2
@greggo Absolutamente. El problema es cuando se manejan funciones que no pueden estar en línea (porque son demasiado grandes). Aquí, la llamada a la devolución de llamada todavía se puede incluir en el caso de un lambda, pero no en el caso de un puntero de función. std::sortEs el ejemplo clásico de esto usando lambdas en lugar de un puntero de función que aquí trae hasta siete veces (¡probablemente más, pero no tengo datos sobre eso!) El rendimiento aumenta.
Konrad Rudolph
1
@greggo Estás confundiendo dos funciones aquí: la que están pasando el lambda a (por ejemplo std::sort, o mapen mi ejemplo) y la propia lambda. La lambda suele ser pequeña. La otra función, no necesariamente. Nos interesan las llamadas en línea a la lambda dentro de la otra función.
Konrad Rudolph
2
@greggo lo sé. Sin embargo, esto es literalmente lo que dice la última oración de mi respuesta.
Konrad Rudolph
1
Lo que me parece curioso (al haber tropezado con él) es que dada una función booleana simple predcuya definición es visible, y usar gcc v5.3, std::find_if(b, e, pred)no está en línea pred, pero std::find_if(b, e, [](int x){return pred(x);})sí. Clang logra alinear ambos, pero no produce código tan rápido como g ++ con el lambda.
rici
26

Porque cuando pasa una "función" a un algoritmo, de hecho, está pasando un puntero para que funcione, por lo que tiene que hacer una llamada indirecta a la función a través del puntero. Cuando usa una lambda, está pasando un objeto a una instancia de plantilla especialmente instanciada para ese tipo y la llamada a la función lambda es una llamada directa, no una llamada a través de un puntero de función, por lo que es mucho más probable que esté en línea.

jcoder
fuente
55
"la llamada a la función lambda es una llamada directa", de hecho. Y lo mismo es cierto para todos los objetos de función, no solo lambdas. Son solo punteros de función que no pueden alinearse tan fácilmente, si es que lo hacen.
Pete Becker