Подтвердить что ты не робот

(Как) Могу ли я встроить конкретный вызов функции?

Скажем, что у меня есть функция, которая вызывается в нескольких частях программы. Позвольте также сказать, что у меня есть специальный вызов этой функции, который находится в чрезвычайно чувствительном к производительности разделе кода (например, цикл, который выполняет итерации десятки миллионов раз и где подсчитывается каждая микросекунда). Есть ли способ, которым я могу заставить complier (gcc в моем случае) встроить этот одиночный, конкретный вызов функции, не встраивая другие?

EDIT: Позвольте мне сделать это совершенно ясно: этот вопрос НЕ о том, чтобы заставить gcc (или любой другой компилятор) встраивать все вызовы функции; скорее, это о том, чтобы запросить, чтобы компилятор ввел конкретный вызов функции.

4b9b3361

Ответ 1

В C (в отличие от С++) нет стандартного способа предположить, что функция должна быть встроена. Это только расширения, специфичные для vender.

Однако вы указываете его, насколько я знаю, компилятор всегда будет пытаться встроить каждый экземпляр, поэтому используйте эту функцию только один раз:

оригинал:

   int MyFunc()  { /* do stuff */  }

измените на:

   inline int MyFunc_inlined()  { /* do stuff */  }

   int MyFunc()  { return MyFunc_inlined(); }

Теперь, в местах, где вы хотите вставить их, используйте MyFunc_inlined()

Примечание. Ключевое слово "inline" в приведенном выше примере является просто заполнителем для любого синтаксиса gcc, используемого для принудительной вставки. Если для ответа на H2CO3 нужно доверять, это будет:

static inline __attribute__((always_inline)) int MyFunc_inlined()  { /* do stuff */  }

Ответ 2

Можно включить вложение на единицу перевода (но не на звонок). Хотя это не ответ на вопрос и является уродливым трюком, он соответствует стандарту C и может быть интересным как связанный материал.

Фокус в том, чтобы использовать определение extern, где вы не хотите встраивать, и extern inline, где вам нужна вставка.

Пример:

$ 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!

Вы также можете использовать нестандартный атрибут (например, __attribute__(always_inline)) в GCC) для определения extern inline вместо того, чтобы полагаться на -O2.

Кстати, трюк используется в glibc.

Ответ 3

традиционный способ принудительного встраивания функции в C состоял в том, чтобы вообще не использовать какую-либо функцию, а использовать такую ​​функцию, как макрос. Этот метод всегда будет встроить функцию, но есть некоторые проблемы с функцией, например макросами. Например:

#define ADD(x, y) ((x) + (y))
printf("%d\n", ADD(2, 2));

Существует также ключевое слово inline, которое было добавлено в C в стандарте C99. Примечательно, что компилятор Microsoft Visual C не поддерживает C99, и поэтому вы не можете использовать inline с этим (жалким) компилятором. Inline только намекает компилятору, что вы хотите, чтобы функция была встроена - она ​​не гарантирует ее.

GCC имеет расширение, которое требует от компилятора встроить функцию.

inline __attribute__((always_inline)) int add(int x, int y) {
    return x + y;
}

Чтобы сделать это чище, вы можете захотеть использовать макрос:

#define ALWAYS_INLINE inline __attribute__((always_inline))
ALWAYS_INLINE int add(int x, int y) {
    return x + y;
}

Я не знаю прямого способа иметь функцию, которая может быть принудительно встроена в определенные вызовы. Но вы можете комбинировать следующие методы:

#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);
}

Или вы можете просто иметь это:

#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;
}

Также обратите внимание, что форсирование встроенной функции может не ускорить работу вашего кода. Встроенные функции вызывают генерацию большего кода, что может привести к возникновению большего количества промахов в кэше. Надеюсь, что это поможет.

Ответ 4

Ответ зависит от вашей функции, того, что вы запрашиваете, и от характера вашей функции. Лучше всего:

  • сообщите компилятору, что вы хотите его встроить
  • сделать функцию статической (будьте осторожны с extern, поскольку семантика немного изменяется в gcc в некоторых режимах)
  • установите параметры компилятора, чтобы проинформировать оптимизатор, который вы хотите вставить, и установите соответствующие лимиты.
  • включить любые не удалось встроить предупреждения в компилятор
  • проверить вывод (вы можете проверить сгенерированный ассемблер), что функция встроена.

подсказки компилятора

Ответы здесь охватывают только одну сторону вложения, языковые намеки на компилятор. Когда в стандарте говорится:

Выполнение функции встроенной функции предполагает, что вызовы функции будут такими, как как можно быстрее. Степень эффективности таких предложений определяется реализацией

Это может быть и для других более сильных подсказок, таких как:

  • GNU __attribute__((always_inline)): Как правило, функции не встроены, если не указана оптимизация. Для функций, объявленных inline, этот атрибут строит функцию, даже если не указан уровень оптимизации.
  • Microsoft __forceinline: ключевое слово __forceinline переопределяет анализ затрат/выгод и вместо этого полагается на решение программиста. Соблюдайте осторожность при использовании __forceinline. Неизбирательное использование __forceinline может привести к большему коду с предельной производительностью или, в некоторых случаях, даже потерями производительности (например, из-за увеличения пейджинга большего исполняемого файла).

Даже оба из них будут полагаться на возможную вставку и, что особенно важно, на флагов компилятора. Для работы с встроенными функциями вам также необходимо понять настройки оптимизации вашего компилятора.

Возможно, стоит сказать, что вложение также может использоваться для обеспечения замены существующих функций только для блока компиляции, в котором вы находитесь. Это можно использовать, когда приблизительные ответы достаточно хороши для вашего алгоритма, или результат может быть достигнут в более быстрый способ с локальными структурами данных.

Встроенное определение обеспечивает альтернативу внешнему определению, которое переводчик может использовать для реализации любой вызов функции в одной и той же единице перевода. Не указано, является ли вызов функция использует встроенное определение или внешнее определение.

Некоторые функции не могут быть встроены

Например, для функций компилятора GNU, которые не могут быть встроены:

Обратите внимание, что определенные определения в определении функции могут сделать его непригодным для встроенной подстановки. Среди этих обычаев есть: вариативные функции, использование alloca, использование типов данных переменной длины (см. Variable Length), использование вычисленных goto (см. "Ярлыки как значения" ), использование нелокальных goto и вложенных функций (см. Вложенные функции). Использование -Winline предупреждает, когда функция, помеченная встроенной, не может быть заменена, и дает причину сбоя.

Таким образом, даже always_inline может не делать то, что вы ожидаете.

Параметры компилятора

Использование встроенных подсказок C99 будет зависеть от того, как вы инструктируете компилятор о строящем поведении, которое вы ищете.

GCC, например, имеет:

-fno-inline, -finline-small-functions, -findirect-inlining, -finline-functions, -finline-functions-called-once, -fearly-inlining, -finline-limit=n

В компиляторе Microsoft также есть опции, которые определяют эффективность встроенного. Некоторые компиляторы также позволят оптимизировать учет рабочего профиля.

Я действительно считаю, что стоит обратить внимание на более широкий контекст оптимизации программы.

Предотвращение вложения

Вы упомянули, что не хотите, чтобы определенные функции были встроены. Это можно сделать, установив что-то вроде __attribute__((always_inline)) без включения оптимизатора. Однако вы, вероятно, захотите оптимизатора. Один из вариантов здесь - это намек на то, что вы этого не хотите: __attribute__ ((noinline)). Но почему это так?

Другие формы оптимизации

Вы также можете подумать о том, как вы можете перестроить свой цикл и избежать ветвей. Прогнозирование ветки может иметь драматический эффект. Для интересной дискуссии об этом см.: Почему быстрее обрабатывать отсортированный массив, чем несортированный массив?

Затем вы также можете развернуть внутренние петли и посмотреть на инварианты.

Ответ 5

Есть источник ядра, который очень удобно использует #define для определения нескольких разных именованных функций с одним и тем же телом. Это решает проблему сохранения двух различных функций. (Я забыл, какой из них был...). Моя идея основана на этом же принципе.

Способ использования определений заключается в том, что вы определяете встроенную функцию в блоке компиляции, в котором вы нуждаетесь. Чтобы продемонстрировать метод, я буду использовать простую функцию:

int add(int a, int b);

Он работает следующим образом: вы создаете генератор функций #define в файле заголовка и объявляете прототип функции нормальной версии функции (тот, который не вложен).

Затем вы объявляете два отдельных генератора функций, один для нормальной функции и один для встроенной функции. Встроенная функция, которую вы объявляете как static __inline__. Когда вам нужно вызвать встроенную функцию в одном из ваших файлов, вы используете генератор define, чтобы получить источник для него. Во всех других файлах вам нужно использовать обычную функцию, вы просто включаете заголовок с прототипом.

Код был протестирован:

Intel(R) Core(TM) i5-3330 CPU @ 3.00GHz
Kernel Version: 3.16.0-49-generic
GCC 4.8.4

Код стоит более тысячи слов, поэтому:

Иерархия файлов

+
| 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);

Это не выглядит красиво, но сокращает работу по поддержанию двух разных функций. Функция полностью определена в макросе GENERATE_ADD(type,prefix), поэтому, если вам когда-либо понадобится изменить функцию, вы измените этот макрос, и все остальное изменится.

Далее, DEFINE_ADD() вызывается из add.c для генерации нормальной версии add. DEFINE_INLINE_ADD() предоставит вам доступ к функции с именем inline_add, которая имеет ту же подпись, что и ваша обычная функция add, но имеет другое имя (inline _).

Примечание. Я не использовал __attribute((always_inline))__ при использовании флага -O3 - __inline__ выполнил задание. Однако, если вы не хотите использовать -O3, используйте:

#define DEFINE_INLINE_ADD()     GENERATE_ADD(static __inline__ __attribute__((always_inline)), inline_)

add.c

#include "add.h"

DEFINE_ADD()

Простой вызов генератора макросов DEFINE_ADD(). Это объявит нормальную версию функции (ту, которая не будет вставлена).

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;
}

Здесь в loop.c вы можете вызвать вызов DEFINE_INLINE_ADD(). Это дает этой функции доступ к функции inline_add. Когда вы скомпилируете, вся функция inline_add будет встроена.

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;
}

Это означает, что вы обычно можете использовать обычную версию add из других файлов.

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;
}

Это означает, что вы можете использовать обе функции в одном и том же модуле компиляции, но одна из функций будет встроена, а другая - не будет (см. GDB disass ниже).

loops.h

/* prototypes for main */
int loop (void);
int loop2 (void);
int loop3 (void);

main.c

#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 [email protected] $^ ${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}

Если вы используете __attribute__((always_inline)), вы можете изменить Makefile на:

CC=gcc
CFLAGS=-Wall -pedantic --std=c11

main: add.o loop.o loop2.o loop3.o main.o
    ${CC} -o [email protected] $^ ${CFLAGS}

%.o: %.c
    ${CC} -c $^ ${CFLAGS}

Компиляция

$ 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

Демонтажные

$ 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 <[email protected]>
   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 <[email protected]>
   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 <[email protected]>
   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 <[email protected]>
   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 <[email protected]>
   0x00000000004006a6 <+102>:cmp    $0x30d43,%ebx
   0x00000000004006ac <+108>:jne    0x400690 <loop3+80>
   0x00000000004006ae <+110>:xor    %eax,%eax
   0x00000000004006b0 <+112>:pop    %rbx
   0x00000000004006b1 <+113>:retq   

Таблица символов

$ 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              [email protected]@GLIBC_2.2.5
00000000004006b2 g     F .text  000000000000005a              main

$ objdump -t main | grep inline
$
 

Хорошо, что это. После 3 часов ударов головой в клавиатуре, пытаясь понять это, это было лучшее, что я мог придумать. Не стесняйтесь указывать какие-либо ошибки, я буду очень благодарен. Мне очень понравился этот встроенный вызов одной функции.

Ответ 6

Вот предложение, напишите тело кода в отдельном файле заголовка. Включите файл заголовка на место, где он должен быть встроенным и в тело в файле C для других вызовов.

void demo(void)
{
#include myBody.h
}

importantloop
{
    // code
#include myBody.h
    // code
}

Ответ 7

Если вы не против иметь два имени для одной и той же функции, вы можете создать небольшую оболочку вокруг своей функции, чтобы "блокировать" атрибут always_inline, влияя на каждый вызов. В моем примере loop_inlined будет именем, которое вы использовали бы в критически важных средах, тогда как обычный loop будет использоваться везде.

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();
}

main.c

#include "inline.h"
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("%d\n", loop_inlined());
    printf("%d\n", loop());
    return 0;
}

Это работает независимо от уровня оптимизации. Компиляция с gcc inline.c main.c на Intel дает:

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>

Первые 7 инструкций являются встроенным вызовом, а обычный вызов имеет 5 инструкций позже.

Ответ 8

Я предполагаю, что ваша функция немного, так как вы хотите встроить ее, если да, то почему бы вам не написать ее в asm?

Что касается вложения только определенного вызова функции, я не думаю, что для вас есть что-то для выполнения этой задачи. Как только функция объявлена ​​как встроенная, и если компилятор будет встроить ее для вас, она будет делать это всюду, когда видит вызов этой функции.