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:
-
Las funciones más grandes y recursivas no están en línea y el compilador ignora la palabra clave en
inline
. -
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 eninline
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:
-
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). -
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 eninline
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.