Перестановка операторов C ++ - PullRequest
6 голосов
/ 14 февраля 2020

Это вопрос об ответе Чендлера здесь (у меня не было достаточно высокого представителя, чтобы комментировать): Обеспечение порядка операторов в C ++

В своем ответе предположим, что foo () не имеет входа или выхода. Это черный ящик, который работает, который в конечном итоге можно наблюдать, но он не понадобится немедленно (например, выполняет некоторый обратный вызов). Таким образом, у нас нет входных / выходных данных локально, чтобы сказать компилятору не оптимизировать. Но я знаю, что foo () где-то изменит память, и в итоге результат будет заметен. Будет ли следующее предотвращать изменение порядка операторов и получить правильное время в этом случае?

#include <chrono>
#include <iostream>

//I believe this tells the compiler that all memory everywhere will be clobbered?
//(from his cppcon talk: https://youtu.be/nXaxk27zwlk?t=2441)
__attribute__((always_inline)) inline void DoNotOptimize() {
  asm volatile("" : : : "memory");
}

// The compiler has full knowledge of the implementation.
static int ugly_global = 1; //we print this to screen sometime later
static void foo(void) { ugly_global *= 2; }

auto time_foo() {
  using Clock = std::chrono::high_resolution_clock;

  auto t1 = Clock::now();         // Statement 1
  DoNotOptimize();
  foo();                          // Statement 2
  DoNotOptimize();
  auto t2 = Clock::now();         // Statement 3

  return t2 - t1;
}

1 Ответ

1 голос
/ 15 февраля 2020

Будет ли следующее предотвращать переупорядочение операторов и получать правильную синхронизацию в этом случае?

Это не должно быть необходимо, поскольку вызовы Clock::now должны на уровне определения языка Соблюдайте порядок. (То есть стандарт C ++ 11 гласит, что часы высокого разрешения должны получать столько информации, сколько система может дать здесь, наиболее удобным способом. См. «Дополнительный вопрос» ниже.)

Но есть более общий случай. Стоит задуматься над вопросом: Как тот, кто предоставляет реализацию библиотеки C ++, на самом деле напишет эту функцию? Или возьмите сам C ++ из уравнения. Учитывая языковой стандарт, как разработчик - человек или группа, пишущие реализацию этого языка - могут получить то, что вам нужно? По сути, нам нужно провести различие между тем, что требует языковой стандарт и , как поставщик реализации реализует требования .

Сам язык может быть выражен в термины абстрактной машины , а языки C и C ++ таковы. Эта абстрактная машина довольно свободно определена: она выполняет какие-то инструкции, которые обращаются к данным, но во многих случаях мы не знаем как она делает эти вещи, или даже насколько велики различные элементы данных ( с некоторыми исключениями для целых чисел фиксированного размера, таких как int64_t) и os on. Машина может иметь или может не иметь «регистров», которые хранят объекты способами, которые не могут быть адресованы, а также памятью, к которой можно обращаться, и адреса которой могут быть записаны в указателях:

p = &var

делает хранилище значений в p (в памяти или регистре) таким образом, чтобы с помощью *p получить доступ к значению, хранящемуся в var (в памяти или регистре) - некоторые машины, особенно в прежние времена, имели / имели адресуемые регистры). 1

Тем не менее, несмотря на всю эту абстракцию, мы хотим запустить реальный код на реальных машинах. Реальные машины имеют реальные ограничения: некоторые инструкции могут требовать определенных значений в определенных регистрах (подумайте обо всех причудливых вещах в наборах команд x86 или целочисленных множителях и делителях с широким результатом, которые используют регистры специального назначения, как на некоторых процессорах MIPS), или вызвать синхронизацию ЦП или что-то еще.

G CC, в частности, изобрел систему ограничений на express, что вы могли или не могли делать на самой машине, используя набор инструкций машины. Со временем это превратилось в доступные для пользователя конструкции asm с секциями ввода, вывода и расширения. Конкретный, который вы показываете:

__attribute__((always_inline)) inline void DoNotOptimize() {
  asm volatile("" : : : "memory");
}

выражает идею, что «эта инструкция» (asm; фактически предоставленная инструкция пуста) «не может быть перемещена» (volatile ) "и забивает всю память компьютера, но не регистры" ("memory" как раздел clobber).

Это не является частью C или C ++ как язык, Это просто конструкция компилятора , поддерживаемая G CC и теперь также поддерживаемая clang. Но достаточно заставить компилятор выдать все хранилища в память до asm и перезагрузить значения из памяти по мере необходимости после asm, если они изменились, когда компьютер выполнил (несуществующую) инструкцию, включенную в строке asm. Нет никакой гарантии, что это будет работать или даже компилироваться в каком-то другом компиляторе, но пока мы реализатор , , мы выбираем компилятор, который мы реализуем для / с.

C ++, поскольку язык теперь поддерживает упорядоченные операции с памятью, которые должен реализовать разработчик. Разработчик может использовать эти asm volatile конструкции для достижения правильного результата, при условии, что они действительно достигают правильного результата. Например, если нам нужно заставить синхронизировать саму машину - создать барьер памяти - мы можем вставить соответствующую машинную инструкцию, такую ​​как mfence или membar #sync или что бы то ни было, в asm. пункт раздела инструкции. См. Также переупорядочение компилятора против переупорядочения памяти как Клаус, упомянутый в комментарии .

Разработчик должен найти подходящий эффективный прием, определяемый компилятором c или нет, чтобы получить правильную семантику при минимизации любого замедления во время выполнения: например, мы могли бы хотеть использовать lfence вместо mfence, если этого достаточно, или membar #LoadLoad, или что-то правильное для машины. Если наша реализация Clock::now требует какого-то необычного встроенного asm, мы пишем один. Если нет, мы не делаем. Мы удостоверяемся, что мы производим то, что требуется, и тогда все пользователей системы могут просто использовать его, без необходимости знать, какие хитрые реализации мы должны были применить.

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

Если компилятор не сделал правильно, то asm заставит его выполнить правильная вещь, в G CC и лязг. Это, вероятно, не будет работать в других компиляторах.


1 В частности, на KA-10 регистры были только первыми шестнадцатью словами памяти. Как отмечает на странице Википедии , это означало, что вы можете поместить туда инструкции и вызвать их. Поскольку первые 16 слов были регистрами, эти инструкции выполнялись намного быстрее, чем другие инструкции.

...