C ветка по оптимизации статических переменных - PullRequest
4 голосов
/ 03 июня 2011

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

У меня есть функция, которая объявляет / определяет static int для известного значения ошибки, которое заставит код принять ветвь.Тем не менее, если функция завершится успешно, я точно знаю, что ветвь больше не будет использоваться.Есть ли оптимизация времени компиляции для этого?В частности, GNU / gcc / glibc?

Итак, у меня есть это:

static unsigned long volatile *getReg(unsigned long addr){

    static int fd = -1;

    if (fd < 0){
        if (fd = open("file", O_RDWR | O_SYNC) < 0){
            return NULL;
        }
    }
}

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

if (__builtin_expect((fd<0),0){

Но из того, что я понимаю, это только подсказка компилятору, и он все еще должен выполнять проверку условия.И я также понимаю, что в 99,9999% случаев этого будет более чем достаточно, чтобы любое дальнейшее увеличение производительности было незначительным.

Мне было интересно, существует ли способ предотвратить даже первую проверку состояния (fd <0) после первого запуска. </p>

Ответы [ 5 ]

4 голосов
/ 03 июня 2011

Короткий ответ - «нет».

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

Ветви дороги только тогда, когда их неправильно предсказывают.__builtin_expect обеспечит, чтобы эта ветвь была неверно предсказана только в первый раз.

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

[update]

Если что-то подобное действительно вызывается миллионы или миллиарды раз в секунду, вы бы справились с этим, реструктурировав свой код для ранней инициализации fdа затем использовать его несколько раз, не потрудившись проверить.Например, вы можете добавить вызов initGlobalState(); в верхней части main () и затем открыть файл.(Вы бы хотели, чтобы соответствующий destroyGlobalState(); закрыл его снова.)

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

В C ++ конструкторы, деструкторы и идиома RAII , кстати, делают такой подход очень естественным.

2 голосов
/ 03 июня 2011

Разделите функцию на две части в своем исходном файле ... и позвольте вызывающему абоненту позаботиться об этом:)

static int fd;

unsigned long volatile *getReg(unsigned long addr) {
  /* do stuff with fd and addr */
  return 0;
}

int getRegSetup(void) {
  fd = open("file", O_RDWR | O_SYNC);
  if (fd < 0) return 1;                /* error */
  /* continue processing */
  return 0;                            /* ok */
}

Вызывающий затем делает

  /* ... */
  if (getRegSetup()) {
    /* error */
  } else {
    do {
      ptr = getReg(42);
    } while (ptr);
  }
  /* ... */
1 голос
/ 03 июня 2011

Ну, один из способов исправить это - использовать указатель на функцию для вызова метода.Инициализируйте функцию ptr для вашей длинной функции и в конце первого вызова установите ее в версию без дополнительной инициализации.

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

0 голосов
/ 22 июня 2011

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

0 голосов
/ 03 июня 2011

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

Я не вижу никакой блокировки в вашем коде, поэтому я предполагаю, что эта функция не должна вызываться из нескольких потоков одновременно. В этом случае вы должны переместить fd из области действия функции, чтобы блокировка с двойной проверкой не применялась. Затем немного перегруппируйте код (это то, что GCC должен делать с подсказками веток, но вы знаете ...). Кроме того, вы можете скопировать файловый дескриптор из основной памяти / строки кэша в регистр, если вы часто обращаетесь к нему. Код будет выглядеть примерно так:

static int g_fd = -1;

static unsigned long volatile *getReg(unsigned long addr)
{
    register int fd = g_fd;

    if (__builtin_expect ((fd > 0), 1))
    {
on_success:
        return NULL; // Do important stuff here.
    }

    fd = open("file", O_RDWR | O_SYNC);

    if (__builtin_expect ((fd > 0), 1))
    {
        g_fd = fd;
        goto on_success;
    }

    return NULL;
}

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

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

...