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

"Статическая константа" против "#define" для эффективности в C

Недавно мне стало интересно, что разница между #define и static const находится в C и почему существуют два метода для выполнения одних и тех же вещей. Я нашел некоторых людей, у которых были похожие вопросы:

Многие люди говорят о лучшей практике и конвенции, а также дают практические причины для использования друг над другом, например, необходимость передать указатель на константу, которую я могу сделать с помощью static const, но не с #define. Однако я еще не нашел, чтобы кто-нибудь говорил о сравнении эффективности этих двух.

Из того, что я понимаю о препроцессоре C, если у меня есть оператор вроде этого:

#define CONSTANT 6

Я создаю постоянное значение, которое можно использовать как это

char[CONSTANT], который фактически будет заменен этим утверждением char[6] до фактического компиляции.

Мне кажется, что это будет более эффективно, чем использование static const constant = 6;, потому что это создаст переменную, называемую константой, которая будет жить в стеке, которое, как я полагаю, будет иметь некоторый багаж, чем #define. Предполагая, что мне нужна константа в ситуации, когда я мог бы использовать либо инструкцию препроцессора #define, либо static const без очевидных причин выбирать один над другим, что более эффективно? И как именно я сам буду тестировать это?

4b9b3361

Ответ 1

Рассмотрим следующие 2 тестовых файла

Test1.c: использует static const foo.

// Test1.c uses static const..

#include <stdio.h>

static const foo = 6;

int main() {
    printf("%d", foo);
    return 0;
}

Test2.c: использует макрос.

// Test2.c uses macro..

#include <stdio.h>

#define foo 6

int main() {
    printf("%d", foo);
    return 0;
}

и соответствующие эквиваленты сборки при использовании gcc -O0 (по умолчанию),

Сборка для Test1.c:

  0000000000000000 <main>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 20             sub    rsp,0x20
   8:   e8 00 00 00 00          call   d <main+0xd>
   d:   b8 06 00 00 00          mov    eax,0x6
  12:   89 c2                   mov    edx,eax
  14:   48 8d 0d 04 00 00 00    lea    rcx,[rip+0x4]        # 1f <main+0x1f>
  1b:   e8 00 00 00 00          call   20 <main+0x20>
  20:   b8 00 00 00 00          mov    eax,0x0
  25:   48 83 c4 20             add    rsp,0x20
  29:   5d                      pop    rbp
  2a:   c3                      ret
  2b:   90                      nop

Сборка для Test2.c:

  0000000000000000 <main>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 20             sub    rsp,0x20
   8:   e8 00 00 00 00          call   d <main+0xd>
   d:   ba 06 00 00 00          mov    edx,0x6
  12:   48 8d 0d 00 00 00 00    lea    rcx,[rip+0x0]        # 19 <main+0x19>
  19:   e8 00 00 00 00          call   1e <main+0x1e>
  1e:   b8 00 00 00 00          mov    eax,0x0
  23:   48 83 c4 20             add    rsp,0x20
  27:   5d                      pop    rbp
  28:   c3                      ret
  29:   90                      nop

В обоих случаях он не использует внешнюю память. Но разница в том, что #define заменяет foo на значение, static const является инструкцией, поэтому он увеличивает указатель на следующую команду и использует 1 дополнительный регистр для хранения значения.

Таким образом, мы можем сказать, что макрос лучше статических констант, но разница минимальна.

EDIT: При использовании опции компиляции -O3 (т.е. при оптимизации) и test1.c и test2.c оценивают то же самое.

0000000000000000 <main>:
   0:   48 83 ec 28             sub    rsp,0x28
   4:   e8 00 00 00 00          call   9 <main+0x9>
   9:   48 8d 0d 00 00 00 00    lea    rcx,[rip+0x0]        # 10 <main+0x10>
  10:   ba 06 00 00 00          mov    edx,0x6
  15:   e8 00 00 00 00          call   1a <main+0x1a>
  1a:   31 c0                   xor    eax,eax
  1c:   48 83 c4 28             add    rsp,0x28
  20:   c3                      ret
  21:   90                      nop

Итак, gcc обрабатывает как static const, так и #define как то же самое, когда оно оптимизируется.

Ответ 2

Если определение константы видимо для перевода, компилятор, безусловно, может использовать это как оптимизацию.

это создало бы переменную, называемую константой, которая будет жить в стеке, который, как я полагаю, будет содержать некоторый багаж, чем #define.

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

Предполагая, что мне нужна константа в ситуации, когда я мог бы использовать либо препроцессор #define, либо статический оператор const без очевидных причин выбирать один над другим, что более эффективно?

Это зависит от компилятора и архитектуры. У меня создается впечатление, что некоторые люди считают, что #define имеет большое преимущество. Это не так. Очевидным случаем является сложная оценка или вызов функции (скажем, sin(4.8)). Рассмотрим константу, используемую внутри цикла. Оценку с правильной областью можно оценивать один раз. Определение может оцениваться на каждой итерации.

И как именно я должен сам тестировать это?

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

Если вы хотите использовать эмпирическое правило, я бы сказал: "Используйте константу, если только #define не обеспечивает заметного улучшения в сценарии".

В документах GCC была хорошая запись об этом. Может быть, кто-то помнит, где именно это было.

Ответ 3

Быстрый способ тестирования простых вопросов оптимизации - использовать godbolt.

Для вашей конкретной проблемы современный оптимизирующий компилятор должен иметь возможность создавать один и тот же код для обоих случаев и фактически просто оптимизирует их до константы. Мы можем видеть это со следующей программой (видеть ее в прямом эфире):

#include <stdio.h>

#define CONSTANT 6
static const int  constant = 6;

void func()
{
  printf( "%d\n", constant ) ;
  printf( "%d\n", CONSTANT ) ;
}

в обоих случаях оба обращения сводятся к следующему:

movl    $6, %esi    #,

Ответ 4

static const переменные не (по крайней мере, не должны быть) созданы в стеке; пространство для них откладывается при загрузке программы, поэтому не должно быть штрафа за выполнение, связанного с их созданием.

Может быть наказание во время выполнения, связанное с их инициализацией. хотя версия gcc, которую я использую, инициализирует константу во время компиляции; Я не знаю, насколько распространено это поведение. Если есть такой штраф во время выполнения, он возникает только один раз при запуске программы.

Кроме того, любая разница в производительности во время выполнения статического const -qualified объекта и литерала 1 (который в конечном итоге расширяет макрос) должна быть незначительной для несуществующих, в зависимости от о типе литерала и операции.

Глупый пример (gcc version 4.1.2 20070115 (SUSE Linux)):

#include <stdio.h>

#define FOO_MACRO 5

static const int foo_const = 5;

int main( void )
{
  printf( "sizeof FOO_MACRO = %zu\n", sizeof FOO_MACRO );
  printf( "sizeof foo_const = %zu\n", sizeof foo_const );
  printf( "      &foo_const = %p\n",  ( void * ) &foo_const );

  printf( "FOO_MACRO = %d\n", FOO_MACRO );
  printf( "foo_const = %d\n", foo_const );

  return 0;
}

Вывод:

sizeof FOO_MACRO = 4
sizeof foo_const = 4
      &foo_const = 0x400660
FOO_MACRO = 5
foo_const = 5

Адрес foo_const находится в разделе .rodata двоичного файла:

[[email protected]]~/prototypes/static: objdump -s -j .rodata static

static:     file format elf64-x86-64

Contents of section .rodata:
 40065c 01000200 05000000 73697a65 6f662046  ........sizeof F
                 ^^^^^^^^
 40066c 4f4f5f4d 4143524f 203d2025 7a750a00  OO_MACRO = %zu..
 40067c 73697a65 6f662066 6f6f5f63 6f6e7374  sizeof foo_const
 40068c 203d2025 7a750a00 20202020 20202666   = %zu..      &f
 40069c 6f6f5f63 6f6e7374 203d2025 700a0046  oo_const = %p..F
 4006ac 4f4f5f4d 4143524f 203d2025 640a0066  OO_MACRO = %d..f
 4006bc 6f6f5f63 6f6e7374 203d2025 640a00    oo_const = %d..

Обратите внимание, что объект уже инициализирован до 5, поэтому нет штрафа за инициализацию во время выполнения.

В инструкциях printf для загрузки значения foo_const в %esi требуется еще один байт, чем тот, который загружает буквенное значение 0x5, и инструкция должна эффективно разыменовать %rip зарегистрироваться:

400538:       be 05 00 00 00          mov    $0x5,%esi
              ^^^^^^^^^^^^^^
40053d:       bf ab 06 40 00          mov    $0x4006ab,%edi
400542:       b8 00 00 00 00          mov    $0x0,%eax
400547:       e8 e4 fe ff ff          callq  400430 <[email protected]>
40054c:       8b 35 0e 01 00 00       mov    270(%rip),%esi        # 400660 <foo_const>
              ^^^^^^^^^^^^^^^^^
400552:       bf bb 06 40 00          mov    $0x4006bb,%edi
400557:       b8 00 00 00 00          mov    $0x0,%eax
40055c:       e8 cf fe ff ff          callq  400430 <[email protected]>

Будет ли это переводиться в измеримую разницу в производительности во время выполнения? Может быть, при правильных обстоятельствах. Если вы делаете что-то, связанное с процессором несколько сотен тысяч раз в узком цикле, то да, используя макрос (который разрешает литерал) над переменной static const может быть значительно быстрее. Если это то, что происходит один раз в течение жизни программы, то разница слишком мала для измерения, и нет никаких веских оснований для использования макроса над переменной static const.

Как всегда, правильность и ремонтопригодность важны не только для производительности 2. Вы вряд ли допустите ошибку, используя static const вместо макроса. Рассмотрим следующий сценарий:

#define FOO 1+2
...
x = FOO * 3;

Какой ответ вы ожидаете и какой ответ вы получите? Сравните это с

static const int foo = 1+2;
...
x = foo * 3;

Да, вы можете исправить случай макроса, используя круглые скобки - (1 + 2). Дело в том, что этот сценарий не является проблемой, если вы используете объект static const. Это еще один способ стрелять в ногу.


1. Сейчас я говорю только о простых скалярных литералах (целых или плавающих), а не о сложных литералах; не исследовали их поведение.

2. Неважно, насколько быстро ваш код, если он дает неправильный ответ или делает неправильную вещь. Неважно, насколько быстро ваш код, если никто не может его исправить или обновить, потому что они не могут понять, как это работает. Неважно, насколько быстро ваш код, если он умирает при первом намеке на плохой ввод. Неважно, насколько быстро ваш код, если он откроет дверь для вредоносного ПО.

Ответ 5

Вы полностью изменили свой вопрос. Вот мой ответ на ваш новый вопрос:

Потому что мы говорим о C и предполагаем, что вы объявляете массив в стеке, ответ на самом деле очень интересный. В этом случае не может быть никакой разницы между ними. "6" на самом деле не используется во время выполнения! Поскольку вы используете его только для определения массива в стеке, компилятор просто использует это для вычисления того, сколько пространства стека требуется для переменной.

Предположим, что у вас есть 32-разрядное адресное пространство, а ваша локальная функция содержит этот 6-байтовый массив (myArray) и 32-битное целое число без знака (myInt). Компилятор создает следующие инструкции для ввода этой функции:  - Напишите адрес возврата 4 байта в стек  - Переместить указатель стека вперед на 10 байт
Во время выполнения функции среда выполнения не знает имен или размеров любых переменных. Если ваш код говорит

myInt = 5;
myArray[myInt] = 25;

тогда компилятор сгенерировал следующие инструкции:

- write 00000000 00000000 00000000 00000101 starting at address (StackPointer - 4)
- write 00001101 starting at (StackPointer - 10 + (value at Stackpointer - 4))

Итак, вы видите, что значение "6" не используется во время выполнения. Фактически, вы можете записать в индекс 6, 7, 8, что бы вы ни хотели. Время выполнения не будет знать, что вы переполняете конец массива. (но в зависимости от того, как вы пишете код, компилятор может поймать ошибку во время компиляции)

Я зачеркнул некоторые детали там (без сомнения, некоторые, о которых я даже не подозреваю), но это суть этого. (Приветствую ваши комментарии)

Определение 6 как "const" может фактически привести к тому, что значение будет сохранено в 4 байта бесполезного пространства, но это не повлияет на выполнение. Obviosuly он будет оптимизирован, потому что он никогда не используется.

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