Ветка C по оптимизации статических переменных

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

У меня есть функция, которая объявляет/определяет статический 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;
        }
    }
}

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

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

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

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


person Falmarri    schedule 02.06.2011    source источник
comment
За исключением самомодифицирующегося кода, я не верю, что есть способ избежать условного (или, что то же самое, указателя на функцию) в ситуации, когда вы ожидаете, что поведение будет отличаться в зависимости от условия!   -  person Oliver Charlesworth    schedule 03.06.2011


Ответы (5)


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

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

person Voo    schedule 03.06.2011
comment
Я принимаю этот ответ, так как он на самом деле звучит как самый точный ответ. Я не думал о переключении указателей функций. - person Falmarri; 22.06.2011
comment
Это также почти наверняка будет медленнее, чем простая реализация, потому что непрямые вызовы медленнее, чем прямые вызовы (потенциально в сотни раз медленнее). Так что это сложнее реализовать, сложнее читать и медленнее... Но в остальном идеальный ответ. (Кстати, именно это я имел в виду, когда упоминал трюки с указателями на функции.) - person Nemo; 22.06.2011
comment
Ага. Я понимаю, что это будет медленнее. Мой вопрос был не столько об оптимизации производительности. Я больше думал о том, что, если проверка условий сама по себе имеет побочные эффекты, которые вам не нужны? Или сама проверка может вызвать ошибку, если она будет запущена после ошибки? (Я знаю, рефакторить проверку/состояние, очевидно =P) - person Falmarri; 22.06.2011

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

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

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

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

[Обновить]

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

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

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

person Nemo    schedule 02.06.2011
comment
+1: Согласен. Конечно, в некоторых ситуациях эта пара циклов может иметь значение, если функция вызывается миллионы раз в секунду. - person Oliver Charlesworth; 03.06.2011
comment
Я согласен с вами обоими. Проверка состояния незначительна. Но, как я уже сказал, это исключительно для образовательных и учебных целей =] - person Falmarri; 03.06.2011
comment
@Oli: За исключением того, что сам вызов функции займет больше времени ... Но я думаю, что добавлю обновление. Спасибо. - person Nemo; 03.06.2011
comment
@Nemo: За исключением случаев, когда он встроен! - person Oliver Charlesworth; 03.06.2011
comment
@Nemo: Иногда, если вызов функции занимает меньше времени, его все же лучше встроить. На самом деле компилятор будет не встраивать, а клонировать и размещать код рядом с вызывающим кодом, что, в свою очередь, может избежать большого объема памяти, перемещаемой туда и обратно между основной памятью и кешем. По-разному. - person ; 03.06.2011
comment
+1 за И, конечно же, дескриптор файла - ужасный пример, потому что все, что вы с ним делаете, в любом случае займет гораздо больше, чем один или два цикла. - person R.. GitHub STOP HELPING ICE; 03.06.2011
comment
@R: дескриптор файла указывает на /dev/mem, который после этого фрагмента кода в этой функции получает mmapped. Так что это НЕ СОВЕРШЕННО надумано. - person Falmarri; 22.06.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);
  }
  /* ... */
person pmg    schedule 02.06.2011
comment
Однако это не отменяет проверки состояния. Он просто перемещает его в другое место. И теперь это на несколько инструкций длиннее, потому что нужно искать адрес функции и т. д. - person Falmarri; 03.06.2011
comment
Условие проверяется только один раз, даже если вам нужно вызвать getReg миллион раз - person pmg; 03.06.2011
comment
@Falmarri, он удаляет его при всех вызовах getReg. - person paxdiablo; 03.06.2011
comment
@paxdiablo: Но вы только что переместили условие на проверку getRegSetup() вместо проверки fd ‹ 0. Да, это конкретное условие проверяется один раз, но условие A по-прежнему проверяется при каждом вызове. - person Falmarri; 03.06.2011
comment
Цикл (добавленный к ответу) находится в блоке else! - person pmg; 03.06.2011
comment
Вы сделали предположение о потоке кода. Петли нет. Эта функция вызывается на основе прерываний процессора. - person Falmarri; 03.06.2011
comment
@pmg, этот пример кода не имеет никакого преимущества, вам лучше показать код, который вызывает setup вне цикла и getreg внутри цикла. - person paxdiablo; 03.06.2011
comment
@Falmarri, если это основано на прерывании, вы можете использовать тот же трюк. Откройте файл перед включением прерываний, а затем полностью удалите проверку из ISR. - person paxdiablo; 03.06.2011
comment
@Falmarri: я сделал предположения, чтобы проиллюстрировать работу кода. Неважно, цикл это или прерывание... просто перемещайте код. - person pmg; 03.06.2011
comment
@paxdiablo: проблема в том, что я не могу контролировать прерывания или когда (в последовательности загрузки) запускается мой код, тем более что он из пользовательского пространства. Мой код просто вызывается в пользовательском пространстве после уведомления о прерывании. Но, как я уже сказал, это на самом деле не связано, поскольку мой вопрос был почти полностью образовательным. На самом деле это часть собственного модуля Python, так что да, просто образовательный вопрос = P - person Falmarri; 22.06.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. Это поможет вам достичь хороших результатов, не тратя слишком много времени на подобные вещи.

person Community    schedule 02.06.2011
comment
__builtin_expect делает больше; на самом деле это помогает компилятору прогнозировать переходы. Предсказание ветвления по умолчанию предполагает, что прямые переходы не выполняются, а обратные переходы выполняются. (Это может динамически изменяться во время выполнения, поскольку ЦП отслеживает, как часто выполнялась каждая ветвь.) Если вы скомпилируете (например) для x86_64 с другими настройками __builtin_expect и посмотрите на сборку, вы увидите, что это именно то, что есть. выполнение (т. е. предположим, что прямая ветвь = не выполнена, обратная ветвь = выполнена). На некоторых архитектурах __builtin_expect фактически устанавливает бит в ветке insn. - person Nemo; 03.06.2011
comment
@Nemo устанавливает ли __builtin_expect предикаты для x86? Не знаю, полезны ли они с современными процессорами или нет, но похоже, что это может быть сделано. - person Voo; 03.06.2011
comment
@Voo: x86 не имеет битов прогнозирования в инструкциях ветвления, но __builtin_expect заставит GCC переупорядочить сгенерированный код, чтобы он соответствовал эвристике прогнозирования ЦП по умолчанию. - person Nemo; 03.06.2011

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

__attribute__((noinline)) static unsigned int volatile *get_mem(unsigned int addr) {
    static void *map = 0 ;
    static unsigned prevPage = -1U ;
    static int fd = -1;
    int poss_err = 0;
    register unsigned page = addr & ~MAP_MASK ;

    if ( unlikely(fd < 0) ) {
        if ((fd = open("/dev/mem", O_RDWR | O_SYNC)) < 0) {
            longjmp(mem_err, errno);
        }
    }
    if ( page != prevPage ) {
        if ( map ) {
            if (unlikely((munmap(map,MAP_SIZE) < 0))) poss_err = 1;
        }
        if (unlikely((map = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, page )) == MAP_FAILED)) longjmp(mem_err, errno);

        prevPage = page ;
    }
    return (unsigned int volatile *)((char *)map+(addr & MAP_MASK));
}

static void set_reg(const struct reg_info * const r, unsigned int val)
{
    unsigned int volatile * const mem = get_mem(r->addr);
    *mem = (*mem & (~(r->mask << r->shift))) | (val << r->shift);
}

// This isn't in the final piece. There are several entry points into this module. Just an example

static int entryPoint(unsigned int value){

    if (setjmp(mem_err)!=0) {
        // Serious error
        return -1;
    }

    for (i=0; i<n; i++) {
        if (strlen(regs[i].name) == strlen(name) &&
                strncmp(regs[i].name, name, strlen (name))==0) {

            set_reg(&regs[i], value);
            return value;
        }
    }
}

Очевидно, это не ответ на вопрос, поскольку он проверяет условие при каждом вызове.

person Falmarri    schedule 22.06.2011