c++ c language-lawyer inline-functions

c++ - Palabra clave "en línea" vs concepto "en línea"



inline function c++ (2)

Estoy haciendo esta pregunta básica para aclarar los registros. Han referido esta pregunta y su respuesta actualmente aceptada , lo cual no es convincente. Sin embargo, la segunda respuesta más votada ofrece una mejor comprensión, pero tampoco es perfecta.

Mientras lee a continuación, distinga entre la palabra clave en inline y el concepto "en inline ".

Aquí está mi opinión:

El concepto en línea

Esto se hace para guardar la sobrecarga de la llamada de una función. Es más similar al reemplazo de código de estilo macro. Nada de lo que se debata.

La palabra clave en inline

Percepción A

La palabra clave en inline es una solicitud al compilador que generalmente se usa para funciones más pequeñas, para que el compilador pueda optimizarla y realizar llamadas más rápidas. El compilador es libre de ignorarlo.

Discuto esto, por las siguientes razones:

  1. Las funciones más grandes y recursivas no están en línea y el compilador ignora la palabra clave en inline .
  2. El optimizador alinea automáticamente las funciones más pequeñas independientemente de que se mencione o no la palabra clave en inline .

Está bastante claro que el usuario no tiene ningún control sobre la función en línea con el uso de la palabra clave en inline .

Percepción B

inline no tiene nada que ver con el concepto de inline. Poner en inline por delante de la función grande / recursiva no ayudará y la función más pequeña no lo necesitará, por estar en línea.

El único uso determinista de inline es mantener la regla de una definición .

es decir, si una función se declara en inline , solo se ordenan las siguientes cosas:

  1. Incluso si su cuerpo se encuentra en varias unidades de traducción (por ejemplo, incluir ese encabezado en varios archivos .cpp ), el compilador generará solo 1 definición y evitará el error del enlazador de múltiples símbolos. (Nota: si los cuerpos de esa función son diferentes, entonces es un comportamiento indefinido).
  2. El cuerpo de la función en inline debe ser visible / accesible en todas las unidades de traducción que lo utilizan. En otras palabras, declarar una función en inline en .h y definir en cualquier archivo .cpp dará como resultado un "error de vinculador de símbolo indefinido" para otros archivos .cpp

Veredicto

La percepción "A" es completamente incorrecta y la percepción "B" es completamente correcta .

Hay algunas citas estándar en esto, sin embargo, espero una respuesta que explique lógicamente si este veredicto es verdadero o falso.


Ambos son correctos.

El uso de en inline puede, o no, influir en la decisión del compilador de en línea cualquier llamada particular a la función. Por lo tanto, A es correcto: actúa como una solicitud no vinculante que invoca llamadas a la función, que el compilador puede ignorar.

El efecto semántico de inline es relajar las restricciones de la regla de una definición para permitir definiciones idénticas en múltiples unidades de traducción, como se describe en B. Para muchos compiladores, esto es necesario para permitir la inclusión de llamadas a funciones: la definición debe estar disponible en ese punto, y los compiladores solo deben procesar una unidad de traducción a la vez.


No estaba seguro acerca de su reclamo:

Las funciones más pequeñas son "alineadas" automáticamente por el optimizador, independientemente de que se mencione o no en línea ... Está bastante claro que el usuario no tiene ningún control sobre la función "en línea" con el uso de la palabra clave en inline .

Escuché que los compiladores pueden ignorar su solicitud en inline , pero no pensé que la ignoraran por completo.

Miré a través del repositorio de Github para descubrir Clang y LLVM. (¡Gracias, software de código abierto!) Descubrí que la palabra clave en inline hace que Clang / LLVM sea más probable que incorpore una función.

La búsqueda

La búsqueda de la palabra en inline en el repositorio de Clang conduce al especificador de token kw_inline . Parece que Clang usa un sistema inteligente basado en macros para construir el lexer y otras funciones relacionadas con palabras clave, por lo que hay una observación directa como if (tokenString == "inline") return kw_inline . Pero aquí, en ParseDecl.cpp , vemos que kw_inline resulta en una llamada a DeclSpec::setFunctionSpecInline() .

case tok::kw_inline: isInvalid = DS.setFunctionSpecInline(Loc, PrevSpec, DiagID); break;

Dentro de esa función , establecemos un bit y emitimos una advertencia si es una inline duplicada:

if (FS_inline_specified) { DiagID = diag::warn_duplicate_declspec; PrevSpec = "inline"; return true; } FS_inline_specified = true; FS_inlineLoc = Loc; return false;

Al buscar FS_inline_specified otro lugar, vemos que es un solo bit en un campo de bits, y se usa en una función getter , isInlineSpecified() :

bool isInlineSpecified() const { return FS_inline_specified | FS_forceinline_specified; }

Al buscar sitios de isInlineSpecified() de isInlineSpecified() , encontramos el codegen , donde convertimos el árbol de análisis C ++ en representación intermedia LLVM:

if (!CGM.getCodeGenOpts().NoInline) { for (auto RI : FD->redecls()) if (RI->isInlineSpecified()) { Fn->addFnAttr(llvm::Attribute::InlineHint); break; } } else if (!FD->hasAttr<AlwaysInlineAttr>()) Fn->addFnAttr(llvm::Attribute::NoInline);

Clang a LLVM

Hemos terminado con la etapa de análisis de C ++. Ahora nuestro especificador en inline se convierte en un atributo del objeto de Function LLVM neutral en lenguaje. Cambiamos de Clang al repositorio LLVM .

Al buscar llvm::Attribute::InlineHint obtiene el método Inliner::getInlineThreshold(CallSite CS) (con un bloque de if paréntesis que da miedo) :

// Listen to the inlinehint attribute when it would increase the threshold // and the caller does not need to minimize its size. Function *Callee = CS.getCalledFunction(); bool InlineHint = Callee && !Callee->isDeclaration() && Callee->getAttributes().hasAttribute(AttributeSet::FunctionIndex, Attribute::InlineHint); if (InlineHint && HintThreshold > thres && !Caller->getAttributes().hasAttribute(AttributeSet::FunctionIndex, Attribute::MinSize)) thres = HintThreshold;

Entonces, ya tenemos un umbral de línea de base desde el nivel de optimización y otros factores, pero si es más bajo que el HintThreshold global de HintThreshold , lo aumentamos. (HintThreshold se puede configurar desde la línea de comando).

getInlineThreshold() parece tener solo un sitio de llamada , un miembro de SimpleInliner :

InlineCost getInlineCost(CallSite CS) override { return ICA->getInlineCost(CS, getInlineThreshold(CS)); }

Llama a un método virtual, también llamado getInlineCost , en su puntero miembro a una instancia de InlineCostAnalysis .

Al buscar ::getInlineCost() para encontrar las versiones que son miembros de la clase, encontramos una que es miembro de AlwaysInline , que es una característica de compilador no estándar pero ampliamente compatible, y otra que es miembro de InlineCostAnalysis . Utiliza su parámetro Threshold here :

CallAnalyzer CA(Callee->getDataLayout(), *TTI, AT, *Callee, Threshold); bool ShouldInline = CA.analyzeCall(CS);

CallAnalyzer::analyzeCall() tiene más de 200 líneas y hace el trabajo real de decidir si la función es en línea . Pesa muchos factores, pero a medida que leemos el método vemos que todos sus cálculos manipulan el Threshold o el Cost . Y al final:

return Cost < Threshold;

Pero el valor de retorno llamado ShouldInline es realmente un nombre inapropiado. De hecho, el propósito principal de analyzeCall() es establecer las variables miembro Cost y Threshold en el objeto CallAnalyzer . El valor de retorno solo indica el caso cuando algún otro factor ha anulado el análisis de costo vs umbral, como vemos aquí :

// Check if there was a reason to force inlining or no inlining. if (!ShouldInline && CA.getCost() < CA.getThreshold()) return InlineCost::getNever(); if (ShouldInline && CA.getCost() >= CA.getThreshold()) return InlineCost::getAlways();

De lo contrario, devolvemos un objeto que almacena el Cost y el Threshold .

return llvm::InlineCost::get(CA.getCost(), CA.getThreshold());

Por lo tanto, no estamos devolviendo una decisión de sí o no en la mayoría de los casos. ¡La búsqueda continúa! ¿Dónde se usa este valor de retorno de getInlineCost() ?

La verdadera decisión

Se encuentra en bool Inliner::shouldInline(CallSite CS) . Otra gran función. Llama a getInlineCost() justo al principio.

Resulta que getInlineCost analiza el costo intrínseco de alinear la función (su firma de argumento, longitud de código, recursión, ramificación, vinculación, etc.) y cierta información agregada sobre cada lugar donde se utiliza la función. Por otro lado, shouldInline() combina esta información con más datos sobre un lugar específico donde se utiliza la función.

En todo el método hay llamadas a InlineCost::costDelta() , que utilizará el valor de Threshold InlineCost tal como lo calcula analyzeCall() . Finalmente, devolvemos un bool . La decisión está hecha. En Inliner::runOnSCC() :

if (!shouldInline(CS)) { emitOptimizationRemarkMissed(CallerCtx, DEBUG_TYPE, *Caller, DLoc, Twine(Callee->getName() + " will not be inlined into " + Caller->getName())); continue; } // Attempt to inline the function. if (!InlineCallIfPossible(CS, InlineInfo, InlinedArrayAllocas, InlineHistoryID, InsertLifetime, DL)) { emitOptimizationRemarkMissed(CallerCtx, DEBUG_TYPE, *Caller, DLoc, Twine(Callee->getName() + " will not be inlined into " + Caller->getName())); continue; } ++NumInlined;

InlineCallIfPossible() realiza la shouldInline() en función de la shouldInline() de shouldInline() .

Por lo tanto, el Threshold se vio afectado por la palabra clave en inline , y se utiliza al final para decidir si se debe incluir en línea.

Por lo tanto, su Percepción B está parcialmente equivocada porque al menos un compilador principal cambia su comportamiento de optimización en función de la palabra clave en inline .

Sin embargo, también podemos ver que en inline es solo una pista, y otros factores pueden superarlo.