Всегда ли проверка состояния эффективна?

#c #performance

#c #Производительность

Вопрос:

  • Предположим, что необходимо только привязать значение к определенному элементу данных определенного объекта, когда bState имеет значение true . Когда bState имеет значение false , это не обязательно, но и не мешает.

Какой из следующих фрагментов кода был бы более эффективным и почему?

(РЕДАКТИРОВАТЬ: обновлено, состояние теперь является членом объекта)

 const int x;     
int i;
int iToBind;
Classname pObject[x];

for (; i < x;   i) {
 if (pObject[i].bState) {
        pObject[i].somedatamember = iToBind;
    }
}
 

Против:

 for (; i < x;   i) {
   pObject[i].somedatamember = iToBind;
}
 

Комментарии:

1. Я бы выбрал последнее, но почему бы просто не запустить обе версии и посмотреть, какая из них быстрее?

2. Я предполагаю, что вы упростили свой код, но, как написано, вы могли бы перенести if (bState) тест за пределы цикла.

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

4. зависит ли bState от pObject[x]?

5. Короче говоря, вы не должны задаваться вопросом об эффективности, но по умолчанию предполагаете, что это достаточно быстро 😉 Никто не может ответить на (1), не попробовав, и единственный способ убедиться в (2) — посмотреть на генерируемый машинный код. Кто-то, знакомый с оптимизациями компилятора (поразительно, как много людей недооценивают их или просто забывают о них), Целевой архитектурой, деталями семантики языка и т. Д., Может сделать обоснованное предположение. Но опять же, это требует некоторого глубокого знания деталей низкого уровня.

Ответ №1:

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

В этой версии:

 for (; i < x;   i) {
  if (pObject[x].bState) {
    pObject[x].somedatamember = iToBind;
  }
}
 

во if время выполнения инструкции происходит остановка, поскольку ЦП должен ждать, пока данные будут считаны из памяти. Скорость чтения памяти зависит от того, где находятся данные. Чем дальше от процессора, тем больше времени требуется: L1 (самый быстрый), L2, L3, Ram, Disk (самый медленный).

В этой версии:

 for (; i < x;   i) {
  pObject[x].somedatamember = iToBind;
}
 

есть только записи в память. Запись в память не приводит к остановке процессора.

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

Комментарии:

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

2. @MatthieuM. Это звучало здорово! Но не могли бы вы объяснить это немного подробнее? : p

3. @MattieuM: В первом случае весь объект не загружается, и, что более важно, загружаемые данные используются только подсистемой ввода-вывода памяти, а не ядром процессора. Вторая форма может полностью обойти кеш.

Ответ №2:

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

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

Ответ №3:

Вы когда-нибудь слышали о циклическом инвариантном движении кода?

Это проход оптимизации от компилятора, который по возможности выводит код из тела циклов.

Например, учитывая следующий код:

 #include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  for (int i = 0; i < argc;   i) {
    if (argc < 100) {
      printf("%dn", atoi(argv[1]));
    }
  }
}
 

Clang генерирует следующий IR:

 define i32 @main(i32 %argc, i8** nocapture %argv) nounwind {
  %1 = icmp sgt i32 %argc, 0
  br i1 %1, label %.lr.ph, label %._crit_edge

.lr.ph:                                           ; preds = %0
  %2 = icmp slt i32 %argc, 100
  %3 = getelementptr inbounds i8** %argv, i64 1
  br i1 %2, label %4, label %._crit_edge

; <label>:4                                       ; preds = %4, %.lr.ph
  %i.01.us = phi i32 [ %9, %4 ], [ 0, %.lr.ph ]
  %5 = load i8** %3, align 8, !tbaa !0
  %6 = tail call i64 @strtol(i8* nocapture %5, i8** null, i32 10) nounwind
  %7 = trunc i64 %6 to i32
  %8 = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([4 x i8]* @.str, i64 0, i64 0), i32 %7) nounwind
  %9 = add nsw i32 %i.01.us, 1
  %exitcond = icmp eq i32 %9, %argc
  br i1 %exitcond, label %._crit_edge, label %4

._crit_edge:                                      ; preds = %4, %.lr.ph, %0
  ret i32 0
}
 

Который можно перевести обратно на C:

 int main(int argc, char** argv) {
  if (argc == 0) { return 0; }

  if (argc >= 100) { return 0; }

  for (int i = 0; i < argc;   i) {
    printf("%dn", atoi(argv[1]));
  }

  return 0;
}
 

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

Редактировать:

Редактирование радикально изменило вопрос (боже, я ненавижу это: p). LICM больше не применяется, и две функции имеют совершенно разные функциональные возможности.

Вывод, однако, остается идентичным. Одна if проверка в for цикле не меняет фундаментальной сложности вашего кода (помните, что условие цикла также проверяется на каждой итерации …).

Комментарии:

1. Извините за плохой пример. Отличный ответ, хотя 🙂 РЕДАКТИРОВАТЬ: и извините за редактирование: p

Ответ №4:

Поскольку то, что я могу сказать bState , не изменяется в цикле в первом фрагменте, поэтому вы можете поместить if outside, что, очевидно, более эффективно.

Ответ №5:

Я бы сказал, что это действительно зависит от контекста. Если важно bState , чтобы во время привязки было true, то дополнительные 1 или 2 инструкции по сборке на итерацию цикла для проверки состояния должны быть оплачены. Если нет, не указывайте if , когда x значение особенно велико.