Функции Pure/const в С++
Я подумываю об использовании функций pure/const более сильно в моем коде на С++. (атрибут pure/const в GCC)
Однако мне любопытно, насколько я должен быть строгим, и что может сломаться.
Наиболее очевидным случаем являются отладочные выходы (в любой форме, может быть на cout, в каком-то файле или в каком-то специальном классе отладки). У меня, вероятно, будет много функций, которые не будут иметь никаких побочных эффектов, несмотря на такой отладочный вывод. Независимо от того, сделан ли вывод отладки или нет, это абсолютно не повлияет на остальную часть моего приложения.
Или другой случай, о котором я думаю, - это использование некоторого класса SmartPointer, который может делать некоторые дополнительные вещи в глобальной памяти при работе в режиме отладки. Если я использую такой объект в функции pure/const, он имеет некоторые незначительные побочные эффекты (в том смысле, что какая-то память, вероятно, будет отличаться), которая не должна иметь никаких реальных побочных эффектов (в том смысле, что поведение любой другой).
Похожие также для мьютексов и других вещей. Я могу думать о многих сложных случаях, когда у него есть некоторые побочные эффекты (в смысле того, что какая-то память будет другой, возможно, даже некоторые потоки будут созданы, некоторые манипуляции с файловой системой сделаны и т.д.), Но не имеет вычислительной разницы (все эти побочные эффекты вполне можно было бы оставить без внимания, и я бы даже предпочел это).
Итак, чтобы суммировать, я хочу отметить функции как чистые /const, которые не являются чистыми /const в строгом смысле. Простой пример:
int foo(int) __attribute__((const));
int bar(int x) {
int sum = 0;
for(int i = 0; i < 100; ++i)
sum += foo(x);
return sum;
}
int foo_callcounter = 0;
int main() {
cout << "bar 42 = " << bar(42) << endl;
cout << "foo callcounter = " << foo_callcounter << endl;
}
int foo(int x) {
cout << "DEBUG: foo(" << x << ")" << endl;
foo_callcounter++;
return x; // or whatever
}
Заметим, что функция foo не const в строгом смысле. Хотя, не имеет значения, что foo_callcounter в конце. Также не имеет значения, если инструкция отладки не выполнена (в случае, если функция не вызывается).
Я ожидал бы выход:
DEBUG: foo(42)
bar 42 = 4200
foo callcounter = 1
И без оптимизации:
DEBUG: foo(42) (100 times)
bar 42 = 4200
foo callcounter = 100
Оба случая полностью прекрасны, потому что для моей usecase имеет значение обратное значение bar (42).
Как это работает на практике? Если я отмечаю такие функции, как pure/const, может ли он сломать что-либо (учитывая, что код верен)?
Обратите внимание, что я знаю, что некоторые компиляторы могут вообще не поддерживать этот атрибут. (BTW., Я собираю их здесь.) Я также знаю, как использовать атрибуты этих атрибутов таким образом, чтобы код оставался переносным (через #defines). Кроме того, все компиляторы, которые мне интересны, каким-то образом поддерживают его; поэтому мне все равно, если мой код работает медленнее с компиляторами, которые этого не делают.
Я также знаю, что оптимизированный код, вероятно, будет выглядеть по-разному в зависимости от компилятора и даже версии компилятора.
Очень актуально также эта статья LWN "Последствия чистых и постоянных функций" , особенно глава "Коды". (Спасибо ArtemGr за подсказку.)
Ответы
Ответ 1
Я ожидал бы выход:
Я бы ожидал ввода:
int bar(int x) {
return foo(x) * 100;
}
Ваш код действительно выглядит странным для меня. Как сторонник, я думаю, что либо foo
действительно имеет побочные эффекты, либо, скорее всего, переписывает его непосредственно на вышеуказанную функцию.
Как это работает на практике? Если я отмечаю такие функции, как pure/const, может ли он сломать что-либо (учитывая, что код верен)?
Если код верен, то нет. Но шансы на правильность вашего кода малы. Если ваш код неверен, эта функция может маскировать ошибки:
int foo(int x) {
globalmutex.lock();
// complicated calculation code
return -1;
// more complicated calculation
globalmutex.unlock();
return x;
}
Теперь, учитывая панель сверху:
int main() {
cout << bar(-1);
}
Это заканчивается с помощью __attribute__((const))
, но взаимоблокировки в противном случае.
Это также сильно зависит от реализации. Например:
void f() {
for(;;)
{
globalmutex.unlock();
cout << foo(42) << '\n';
globalmutex.lock();
}
}
Где компилятор должен переместить вызов foo(42)
? Разрешено ли оптимизировать этот код? Не в общем! Поэтому, если цикл не является тривиальным, у вас нет преимуществ от вашей функции. Но если ваш цикл тривиален, вы можете легко его оптимизировать самостоятельно.
EDIT: поскольку Альберт попросил менее очевидную ситуацию, вот он:
F
или пример, если вы реализуете operator <<
для ostream, вы используете ostream:: sentry, который блокирует буфер потока. Предположим, вы вызываете pure/const f
после, который вы отпустили, или до, вы заблокировали его. Кто-то использует этот оператор cout << YourType()
, а f
также использует cout << "debug info"
. По вашему мнению, компилятор может поместить вызов f
в критический раздел. Выполняется взаимоблокировка.
Ответ 2
Я подумываю об использовании чистых/const-функций в моем коде на С++.
Это скользкий склон. Эти атрибуты нестандартные, и их преимущество ограничено в основном микро-оптимизацией.
Это не хороший компромисс. Вместо этого напишите чистый код, не применяйте такие микрооптимизации, если вы не профилировали тщательно, а theres нет. Или совсем нет.
Обратите внимание на то, что в принципе эти атрибуты довольно хороши, потому что они явно подразумевают предположения о функциях как для компилятора, так и для программиста. Это хорошо. Однако существуют и другие методы, позволяющие сделать подобные предположения явными (включая документацию). Но поскольку эти атрибуты нестандартны, у них нет места в нормальном коде. Они должны быть ограничены очень разумным использованием в критичных по производительности библиотеках, где автор пытается исправить лучший код для каждого компилятора. То есть, писатель знает о том, что только GCC может использовать эти атрибуты и сделал разные варианты для других компиляторов.
Ответ 3
Вы можете определенно нарушить переносимость вашего кода. И почему вы хотите реализовать свой собственный умный указатель - изучение опыта отдельно? Разве их недостаточно для вас в (близких) стандартных библиотеках?
Ответ 4
Я думаю, что никто не знает этого (за исключением gcc-программистов) просто потому, что вы полагаетесь на undefined и недокументированное поведение, которое может меняться от версии к версии. Но как насчет этого:
#ifdef NDEBUG \
#define safe_pure __attribute__((pure)) \
#else \
#define safe_pure \
#endif
Я знаю, что это не совсем то, что вы хотите, но теперь вы можете использовать атрибут pure, не нарушая правил.
Если вы хотите узнать ответ, вы можете спросить на форумах gcc (список рассылки, что угодно), они должны быть в состоянии дать вам точный ответ.
Значение кода: Когда определен NDEBUG (символ, используемый в макросах assert), мы не отлаживаем, не имеем побочных эффектов, можем использовать чистый атрибут. Когда он определен, у нас есть побочные эффекты, поэтому он не будет использовать чистый атрибут.
Ответ 5
Я бы рассмотрел сгенерированный asm, чтобы узнать, какая разница. (Мое предположение заключалось в том, что переход от потоков С++ к чему-то другому принесет большую пользу, см. http://typethinker.blogspot.com/2010/05/are-c-iostreams-really-slow.html)