uber - (¿Cómo) puedo en línea una llamada de función particular?
superponer graficas en r (8)
Aquí hay una sugerencia, escriba el cuerpo del código en un archivo de encabezado separado. Incluya el archivo de encabezado en el lugar donde debe estar en línea y en un cuerpo en un archivo C para otras llamadas.
void demo(void)
{
#include myBody.h
}
importantloop
{
// code
#include myBody.h
// code
}
Digamos que tengo una función que se llama en varias partes de un programa. Digamos también que tengo una llamada particular a esa función que se encuentra en una sección de código extremadamente sensible al rendimiento (por ejemplo, un bucle que se repite decenas de millones de veces y donde cada microsegundo cuenta). ¿Hay alguna forma en que pueda forzar al complaciente ( gcc
en mi caso) a incluir esa llamada a una sola función en particular, sin incluir a los demás?
EDITAR: Permítame dejarlo completamente claro: esta pregunta NO se trata de forzar a gcc (o cualquier otro compilador) a incluir en línea todas las llamadas a una función; más bien, se trata de solicitar que el compilador en línea una llamada particular a una función.
En C (a diferencia de C ++) no hay una manera estándar de sugerir que una función deba estar en línea. Solo se trata de extensiones específicas para el vendedor.
Sin embargo, usted lo especifica, por lo que sé, el compilador siempre intentará integrar cada instancia, así que use esa función solo una vez:
original:
int MyFunc() { /* do stuff */ }
cambiar a:
inline int MyFunc_inlined() { /* do stuff */ }
int MyFunc() { return MyFunc_inlined(); }
Ahora, en los lugares donde desea que esté en línea, use MyFunc_inlined()
Nota: la palabra clave "en línea" de lo anterior es solo un marcador de posición para cualquier sintaxis que gcc use para forzar una inserción. Si la respuesta eliminada de H2CO3 es confiable, eso sería:
static inline __attribute__((always_inline)) int MyFunc_inlined() { /* do stuff */ }
Es posible habilitar el ingreso por unidad de traducción (pero no por llamada). Aunque esta no es una respuesta para la pregunta y es un truco feo, cumple con el estándar C y puede ser interesante como cosas relacionadas.
El truco consiste en utilizar la definición extern
donde no desea la línea y la extern inline
donde la necesita.
Ejemplo:
$ cat func.h
int func();
$ cat func.c
int func() { return 10; }
$ cat func_inline.h
extern inline int func() { return 5; }
$ cat main.c
#include <stdio.h>
#ifdef USE_INLINE
# include "func_inline.h"
#else
# include "func.h"
#endif
int main() { printf("%d/n", func()); return 0; }
$ gcc main.c func.c && ./a.out
10 // non-inlined version
$ gcc main.c func.c -DUSE_INLINE && ./a.out
10 // non-inlined version
$ gcc main.c func.c -DUSE_INLINE -O2 && ./a.out
5 // inlined!
También puede usar un atributo no estándar (por ejemplo, __attribute__(always_inline))
en GCC) para la definición en extern inline
, en lugar de confiar en -O2
.
Por cierto, el truco se utiliza en glibc .
Hay una fuente del núcleo que usa #define
s de una manera muy interesante para definir varias funciones con nombres diferentes con el mismo cuerpo . Esto resuelve el problema de tener dos funciones diferentes para mantener . (Olvidé de cuál era ...). Mi idea se basa en este mismo principio.
La forma de utilizar las definiciones es que definirá la función en línea en la unidad de compilación que necesita. Para demostrar el método usaré una función simple:
int add(int a, int b);
Funciona así: crea un generador de funciones #define
en un archivo de encabezado y declara el prototipo de función de la versión normal de la función (la que no está en línea ).
Luego declara dos generadores de funciones separados , uno para la función normal y otro para la función en línea. La función en línea que declara como static __inline__
. Cuando necesita llamar a la función en línea en uno de sus archivos, usa la definición del generador para obtener la fuente para ello. En todos los demás archivos que necesite para utilizar la función normal, simplemente incluya el encabezado con el prototipo.
El código fue probado en:
Intel(R) Core(TM) i5-3330 CPU @ 3.00GHz
Kernel Version: 3.16.0-49-generic
GCC 4.8.4
El código vale más que mil palabras, entonces:
Jerarquía de archivos
+
| Makefile
| add.h
| add.c
| loop.c
| loop2.c
| loop3.c
| loops.h
| main.c
add.h
#define GENERATE_ADD(type, prefix) /
type int prefix##add(int a, int b) { return a + b; }
#define DEFINE_ADD() GENERATE_ADD(,)
#define DEFINE_INLINE_ADD() GENERATE_ADD(static __inline__, inline_)
int add(int, int);
Esto no se ve bien, pero reduce el trabajo de mantener dos funciones diferentes. La función está completamente definida dentro de la macro GENERATE_ADD(type,prefix)
, por lo que si alguna vez necesita cambiar la función, cambie esta macro y todo lo demás cambiará.
A continuación, se DEFINE_ADD()
desde add.c
para generar la versión normal de add
. DEFINE_INLINE_ADD()
le dará acceso a una función llamada inline_add
, que tiene la misma firma que su función de add
normal, pero tiene un nombre diferente (el prefijo inline_ ).
Nota: No __attribute((always_inline))__
el __attribute((always_inline))__
cuando __inline__
el __inline__
: el __inline__
hizo el trabajo. Sin embargo, si no quieres usar -O3
, usa:
#define DEFINE_INLINE_ADD() GENERATE_ADD(static __inline__ __attribute__((always_inline)), inline_)
add.c
#include "add.h"
DEFINE_ADD()
Llamada simple al generador de macros DEFINE_ADD()
. Esto declarará la versión normal de la función (la que no se insertará).
loop.c
#include <stdio.h>
#include "add.h"
DEFINE_INLINE_ADD()
int loop(void)
{
register int i;
for (i = 0; i < 100000; i++)
printf("%d/n", inline_add(i + 1, i + 2));
return 0;
}
Aquí en loop.c
puede ver la llamada a DEFINE_INLINE_ADD()
. Esto le da a esta función acceso a la función inline_add
. Cuando compile, toda la función inline_add
estará en línea.
loop2.c
#include <stdio.h>
#include "add.h"
int loop2(void)
{
register int i;
for (i = 0; i < 100000; i++)
printf("%d/n", add(i + 1, i + 2));
return 0;
}
Esto es para mostrar que puede usar la versión normal de add
normalmente desde otros archivos.
loop3.c
#include <stdio.h>
#include "add.h"
DEFINE_INLINE_ADD()
int loop3(void)
{
register int i;
printf ("add: %d/n", add(2,3));
printf ("add: %d/n", add(4,5));
for (i = 0; i < 100000; i++)
printf("%d/n", inline_add(i + 1, i + 2));
return 0;
}
Esto es para mostrar que puede usar ambas funciones en la misma unidad de compilación , sin embargo, una de las funciones estará en línea y la otra no (vea GDB a continuación para ver detalles).
bucles.h
/* prototypes for main */
int loop (void);
int loop2 (void);
int loop3 (void);
C Principal
#include <stdio.h>
#include <stdlib.h>
#include "add.h"
#include "loops.h"
int main(void)
{
printf("%d/n", add(1,2));
printf("%d/n", add(2,3));
loop();
loop2();
loop3();
return 0;
}
Makefile
CC=gcc
CFLAGS=-Wall -pedantic --std=c11
main: add.o loop.o loop2.o loop3.o main.o
${CC} -o $@ $^ ${CFLAGS}
add.o: add.c
${CC} -c $^ ${CFLAGS}
loop.o: loop.c
${CC} -c $^ -O3 ${CFLAGS}
loop2.o: loop2.c
${CC} -c $^ ${CFLAGS}
loop3.o: loop3.c
${CC} -c $^ -O3 ${CFLAGS}
Si usa __attribute__((always_inline))
puede cambiar el Makefile
a:
CC=gcc
CFLAGS=-Wall -pedantic --std=c11
main: add.o loop.o loop2.o loop3.o main.o
${CC} -o $@ $^ ${CFLAGS}
%.o: %.c
${CC} -c $^ ${CFLAGS}
Compilacion
$ make
gcc -c add.c -Wall -pedantic --std=c11
gcc -c loop.c -O3 -Wall -pedantic --std=c11
gcc -c loop2.c -Wall -pedantic --std=c11
gcc -c loop3.c -O3 -Wall -pedantic --std=c11
gcc -Wall -pedantic --std=c11 -c -o main.o main.c
gcc -o main add.o loop.o loop2.o loop3.o main.o -Wall -pedantic --std=c11
Desmontaje
$ gdb main
(gdb) disass add
0x000000000040059d <+0>: push %rbp
0x000000000040059e <+1>: mov %rsp,%rbp
0x00000000004005a1 <+4>: mov %edi,-0x4(%rbp)
0x00000000004005a4 <+7>: mov %esi,-0x8(%rbp)
0x00000000004005a7 <+10>:mov -0x8(%rbp),%eax
0x00000000004005aa <+13>:mov -0x4(%rbp),%edx
0x00000000004005ad <+16>:add %edx,%eax
0x00000000004005af <+18>:pop %rbp
0x00000000004005b0 <+19>:retq
(gdb) disass loop
0x00000000004005c0 <+0>: push %rbx
0x00000000004005c1 <+1>: mov $0x3,%ebx
0x00000000004005c6 <+6>: nopw %cs:0x0(%rax,%rax,1)
0x00000000004005d0 <+16>:mov %ebx,%edx
0x00000000004005d2 <+18>:xor %eax,%eax
0x00000000004005d4 <+20>:mov $0x40079d,%esi
0x00000000004005d9 <+25>:mov $0x1,%edi
0x00000000004005de <+30>:add $0x2,%ebx
0x00000000004005e1 <+33>:callq 0x4004a0 <__printf_chk@plt>
0x00000000004005e6 <+38>:cmp $0x30d43,%ebx
0x00000000004005ec <+44>:jne 0x4005d0 <loop+16>
0x00000000004005ee <+46>:xor %eax,%eax
0x00000000004005f0 <+48>:pop %rbx
0x00000000004005f1 <+49>:retq
(gdb) disass loop2
0x00000000004005f2 <+0>: push %rbp
0x00000000004005f3 <+1>: mov %rsp,%rbp
0x00000000004005f6 <+4>: push %rbx
0x00000000004005f7 <+5>: sub $0x8,%rsp
0x00000000004005fb <+9>: mov $0x0,%ebx
0x0000000000400600 <+14>:jmp 0x400625 <loop2+51>
0x0000000000400602 <+16>:lea 0x2(%rbx),%edx
0x0000000000400605 <+19>:lea 0x1(%rbx),%eax
0x0000000000400608 <+22>:mov %edx,%esi
0x000000000040060a <+24>:mov %eax,%edi
0x000000000040060c <+26>:callq 0x40059d <add>
0x0000000000400611 <+31>:mov %eax,%esi
0x0000000000400613 <+33>:mov $0x400794,%edi
0x0000000000400618 <+38>:mov $0x0,%eax
0x000000000040061d <+43>:callq 0x400470 <printf@plt>
0x0000000000400622 <+48>:add $0x1,%ebx
0x0000000000400625 <+51>:cmp $0x1869f,%ebx
0x000000000040062b <+57>:jle 0x400602 <loop2+16>
0x000000000040062d <+59>:mov $0x0,%eax
0x0000000000400632 <+64>:add $0x8,%rsp
0x0000000000400636 <+68>:pop %rbx
0x0000000000400637 <+69>:pop %rbp
0x0000000000400638 <+70>:retq
(gdb) disass loop3
0x0000000000400640 <+0>: push %rbx
0x0000000000400641 <+1>: mov $0x3,%esi
0x0000000000400646 <+6>: mov $0x2,%edi
0x000000000040064b <+11>:mov $0x3,%ebx
0x0000000000400650 <+16>:callq 0x40059d <add>
0x0000000000400655 <+21>:mov $0x400798,%esi
0x000000000040065a <+26>:mov %eax,%edx
0x000000000040065c <+28>:mov $0x1,%edi
0x0000000000400661 <+33>:xor %eax,%eax
0x0000000000400663 <+35>:callq 0x4004a0 <__printf_chk@plt>
0x0000000000400668 <+40>:mov $0x5,%esi
0x000000000040066d <+45>:mov $0x4,%edi
0x0000000000400672 <+50>:callq 0x40059d <add>
0x0000000000400677 <+55>:mov $0x400798,%esi
0x000000000040067c <+60>:mov %eax,%edx
0x000000000040067e <+62>:mov $0x1,%edi
0x0000000000400683 <+67>:xor %eax,%eax
0x0000000000400685 <+69>:callq 0x4004a0 <__printf_chk@plt>
0x000000000040068a <+74>:nopw 0x0(%rax,%rax,1)
0x0000000000400690 <+80>:mov %ebx,%edx
0x0000000000400692 <+82>:xor %eax,%eax
0x0000000000400694 <+84>:mov $0x40079d,%esi
0x0000000000400699 <+89>:mov $0x1,%edi
0x000000000040069e <+94>:add $0x2,%ebx
0x00000000004006a1 <+97>:callq 0x4004a0 <__printf_chk@plt>
0x00000000004006a6 <+102>:cmp $0x30d43,%ebx
0x00000000004006ac <+108>:jne 0x400690 <loop3+80>
0x00000000004006ae <+110>:xor %eax,%eax
0x00000000004006b0 <+112>:pop %rbx
0x00000000004006b1 <+113>:retq
Tabla de símbolos
$ objdump -t main | grep add
0000000000000000 l df *ABS* 0000000000000000 add.c
000000000040059d g F .text 0000000000000014 add
$ objdump -t main | grep loop
0000000000000000 l df *ABS* 0000000000000000 loop.c
0000000000000000 l df *ABS* 0000000000000000 loop2.c
0000000000000000 l df *ABS* 0000000000000000 loop3.c
00000000004005c0 g F .text 0000000000000032 loop
00000000004005f2 g F .text 0000000000000047 loop2
0000000000400640 g F .text 0000000000000072 loop3
$ objdump -t main | grep main
main: file format elf64-x86-64
0000000000000000 l df *ABS* 0000000000000000 main.c
0000000000000000 F *UND* 0000000000000000 __libc_start_main@@GLIBC_2.2.5
00000000004006b2 g F .text 000000000000005a main
$ objdump -t main | grep inline
$
Bueno, eso es todo. Después de 3 horas de golpear mi cabeza en el teclado tratando de resolverlo, esto fue lo mejor que pude encontrar. Siéntase libre de señalar cualquier error, realmente lo apreciaré. Me interesé mucho en esta llamada particular de una función en línea .
La respuesta es que depende de su función, lo que solicite y la naturaleza de su función. Su mejor apuesta es:
- dile al compilador que lo quieres en línea
- haga que la función sea estática (tenga cuidado con extern ya que su semántica cambia un poco en gcc en algunos modos)
- configure las opciones del compilador para informar al optimizador que desea incluir y establezca los límites en línea adecuadamente
- activar cualquiera de las advertencias no pudo en línea en el compilador
- verifique la salida (usted podría verificar el ensamblador generado) que la función está en línea.
Consejos del compilador
Las respuestas aquí cubren solo un lado de la alineación, el lenguaje alude al compilador. Cuando la norma dice:
Hacer una función una función en línea sugiere que las llamadas a la función sean lo más rápidas posible. La medida en que dichas sugerencias son efectivas está definida por la implementación
Este puede ser el caso para otros consejos más fuertes, tales como:
-
__attribute__((always_inline))
: Generalmente, las funciones no están en línea a menos que se especifique la optimización. Para funciones declaradas en línea, este atributo alinea la función incluso si no se especificó un nivel de optimización. -
__forceinline
de Microsoft: la palabra clave __forceinline anula el análisis de costo / beneficio y se basa en el juicio del programador. Tenga cuidado al usar __forceinline. El uso indiscriminado de __forceinline puede dar como resultado un código más grande con solo ganancias de rendimiento marginales o, en algunos casos, incluso pérdidas de rendimiento (debido a la mayor paginación de un ejecutable más grande, por ejemplo).
Incluso ambos podrían depender de la posibilidad de alineación, y de manera crucial en las banderas del compilador. Para trabajar con funciones en línea, también necesita comprender la configuración de optimización de su compilador.
Puede valer la pena decir que la inserción también se puede usar para reemplazar las funciones existentes solo para la unidad de compilación en la que se encuentra. Esto se puede usar cuando las respuestas aproximadas son lo suficientemente buenas para su algoritmo, o se puede lograr un resultado de una manera más rápida Con estructuras de datos locales.
Una definición en línea proporciona una alternativa a una definición externa, que un traductor puede usar para implementar cualquier llamada a la función en la misma unidad de traducción. No se especifica si una llamada a la función utiliza la definición en línea o la definición externa.
Algunas funciones no pueden ser en línea
Por ejemplo, para las funciones del compilador GNU que no pueden ser en línea son:
Tenga en cuenta que ciertos usos en una definición de función pueden hacerla inadecuada para la sustitución en línea. Entre estos usos se encuentran: funciones variables, uso de aloca, uso de tipos de datos de longitud variable (ver Longitud variable), uso de goto computado (ver Etiquetas como valores), uso de goto no local y funciones anidadas (ver Funciones anidadas). El uso de -Winline advierte cuando una función marcada en línea no puede ser sustituida, y da la razón de la falla.
Así que incluso always_inline
puede no hacer lo que esperas.
Opciones del compilador
El uso de las sugerencias en línea de C99 dependerá de que usted le indique al compilador el comportamiento en línea que está buscando.
GCC, por ejemplo, tiene:
-fno-inline
, -finline-small-functions
, -findirect-inlining
, -finline-functions
, -finline-functions-called-once
, -fearly-inlining
, -finline-limit=n
El compilador de Microsoft también tiene opciones que dictan la efectividad de la línea. Algunos compiladores también permitirán que la optimización tenga en cuenta el perfil de ejecución.
Creo que vale la pena verlo en el contexto más amplio de la optimización del programa.
Prevención de la inclinación
Usted menciona que no quiere que ciertas funciones estén en línea. Esto se puede hacer configurando algo como __attribute__((always_inline))
sin encender el optimizador. Sin embargo, probablemente querría el optimizador. Una opción aquí sería insinuar que no lo desea: __attribute__ ((noinline))
. Pero ¿por qué este sería el caso?
Otras formas de optimización
También puede considerar cómo podría reestructurar su bucle y evitar ramas. La predicción de rama puede tener un efecto dramático. Para una discusión interesante sobre esto, vea: ¿Por qué es más rápido procesar una matriz ordenada que una matriz sin clasificar?
Entonces también puede hacer que los bucles internos sean más pequeños para desenrollarlos y mirar invariantes.
Si no le importa tener dos nombres para la misma función, puede crear una pequeña envoltura alrededor de su función para "bloquear" que el atributo always_inline afecte a cada llamada. En mi ejemplo, loop_inlined
sería el nombre que usaría en las secciones críticas para el rendimiento, mientras que el loop
simple se usaría en cualquier otro lugar.
inline.h
#include <stdlib.h>
static inline int loop_inlined() __attribute__((always_inline));
int loop();
static inline int loop_inlined() {
int n = 0, i;
for(i = 0; i < 10000; i++)
n += rand();
return n;
}
inline.c
#include "inline.h"
int loop() {
return loop_inlined();
}
C Principal
#include "inline.h"
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("%d/n", loop_inlined());
printf("%d/n", loop());
return 0;
}
Esto funciona independientemente del nivel de optimización. Compilar con gcc inline.c main.c
en Intel da:
4011e6: c7 44 24 18 00 00 00 movl $0x0,0x18(%esp)
4011ed: 00
4011ee: eb 0e jmp 4011fe <_main+0x2e>
4011f0: e8 5b 00 00 00 call 401250 <_rand>
4011f5: 01 44 24 1c add %eax,0x1c(%esp)
4011f9: 83 44 24 18 01 addl $0x1,0x18(%esp)
4011fe: 81 7c 24 18 0f 27 00 cmpl $0x270f,0x18(%esp)
401205: 00
401206: 7e e8 jle 4011f0 <_main+0x20>
401208: 8b 44 24 1c mov 0x1c(%esp),%eax
40120c: 89 44 24 04 mov %eax,0x4(%esp)
401210: c7 04 24 60 30 40 00 movl $0x403060,(%esp)
401217: e8 2c 00 00 00 call 401248 <_printf>
40121c: e8 7f ff ff ff call 4011a0 <_loop>
401221: 89 44 24 04 mov %eax,0x4(%esp)
401225: c7 04 24 60 30 40 00 movl $0x403060,(%esp)
40122c: e8 17 00 00 00 call 401248 <_printf>
Las primeras 7 instrucciones son la llamada en línea, y la llamada normal ocurre 5 instrucciones más tarde.
Supongo que su función es pequeña ya que quiere alinearla, si es así, ¿por qué no la escribe en asm?
En cuanto a incluir solo una llamada específica a una función, no creo que exista algo para hacer esta tarea por usted. Una vez que una función se declara como en línea y si el compilador la alinea por ti, lo hará en cualquier lugar que vea una llamada a esa función.
la forma tradicional de forzar una función en línea en C era no usar una función, sino usar una función como macro. Este método siempre integrará la función, pero hay algunos problemas con la función como las macros. Por ejemplo:
#define ADD(x, y) ((x) + (y))
printf("%d/n", ADD(2, 2));
También está la palabra clave en línea , que se agregó a C en el estándar C99. Cabe destacar que el compilador de Visual C de Microsoft no es compatible con C99 y, por lo tanto, no se puede usar en línea con ese compilador (miserable). Inline solo sugiere al compilador que desea que la función esté en línea, no lo garantiza.
GCC tiene una extensión que requiere que el compilador incorpore la función.
inline __attribute__((always_inline)) int add(int x, int y) {
return x + y;
}
Para hacer esto más limpio, es posible que desee utilizar una macro:
#define ALWAYS_INLINE inline __attribute__((always_inline))
ALWAYS_INLINE int add(int x, int y) {
return x + y;
}
No conozco una forma directa de tener una función que se pueda forzar en ciertas llamadas. Pero puedes combinar las técnicas como esta:
#define ALWAYS_INLINE inline __attribute__((always_inline))
#define ADD(x, y) ((x) + (y))
ALWAYS_INLINE int always_inline_add(int x, int y) {
return ADD(x, y);
}
int normal_add(int x, int y) {
return ADD(x, y);
}
O, simplemente podrías tener esto:
#define ADD(x, y) ((x) + (y))
int add(int x, int y) {
return ADD(x, y);
}
int main() {
printf("%d/n", ADD(2,2)); // always inline
printf("%d/n", add(2,2)); // normal function call
return 0;
}
Además, tenga en cuenta que forzar el ingreso de una función podría no hacer que su código sea más rápido. Las funciones en línea hacen que se genere un código más grande, lo que podría causar que se produzcan más fallas de caché. Espero que eso ayude.