Я определил функцию в C следующим образом:
char **fun(size_t *size) {
size_t local_size = 0;
while (...) {
//Function code...
local_size++;
}
*size = local_size;
//Code for returning
}
Как видите, у него есть локальная переменная, которая служит счетчиком и затем возвращает ее, устанавливая значение ссылки на размер на значение локального размера.
Кроме того, я реализовал функцию, работая напрямую со значением, указанным во входной ссылке, например:
char **fun(size_t *size) {
*size = 0;
while (...) {
//Function code...
(*size)++;
}
//Code for returning
}
Учитывая, что они ведут себя одинаково, какой вариант будет более эффективным, если я знаю, что к переменной внутри функции будет много обращений? Быстрее ли работать с опцией локальной переменной, чтобы воспользоваться преимуществами выделения памяти стека?
Вполне вероятно, что оптимизатор поймет, что *size можно кэшировать в регистре, поэтому обе версии будут создавать один и тот же код. @CraigEstey
@barmar Я использую флаг -O3 в компиляторе gcc, гарантирует ли это, что компилятор автоматически кеширует *size?
Это зависит от того, вызываете ли вы другие функции в цикле. Если вы это сделаете, он не может быть уверен, что эти функции не ссылаются на *size, поэтому ему придется присвоить переменную.
Возможно, объявление size_t *restrict size предотвратит это.
@Barmar Более подробно об этом можно узнать из недавнего вопроса: Почему GCC генерирует перемещение начала массива на каждой итерации цикла для доступа к массиву с помощью []? (-O3, x86) Я предпочитаю не предполагать, что оптимизатор «поступит правильно» в таких случаях. На мой взгляд, использование local_size — лучший/более понятный вариант с точки зрения производительности и ясности кода/намерений.





Если //Function code... содержит что-либо, что может получить доступ к тому, на что указывает size, компилятор не сможет оптимизировать путем кэширования копии *size:
size_t, size может указывать на этот объект.*size.*size, поскольку символьным типам разрешен доступ к любым объектам.size, на который указывает.Итак, вам лучше использовать первую версию функции.
Будет ли квалификатор restrict сообщать компилятору, что подобные псевдонимы невозможны? Или речь идет только о последнем пункте?
restrict хотел бы обратиться к некоторым из них, верно?
@Barmar: Re: «Сообщит ли квалификатор restrict компилятору, что ни один из этих псевдонимов не может произойти?»: Да, но это то, что вы хотите сказать компилятору? На простом примере ОП, возможно, да. Но предположим, что что-то еще записывает в *size во время процедуры, но вас это не волнует, и вы просто хотите сохранить окончательный счетчик там, когда процедура будет завершена, даже если какая-то подпрограмма тем временем использовала его для временного хранения. Это достигается путем кэширования и сохранения в конце. Напротив, добавление restrict делает поведение программы неопределенным. В случаях, когда есть другие параметры,…
… можно учитывать и другие взаимодействия. Так что ответ на этот вопрос — не просто добавить restrict. Достаточно легко сохранить локальный счетчик, сохранить его в конце и сохранить код простым и понятным.
Если другому коду необходимо использовать инкрементальные значения size, вы не сможете кэшировать их в локальную переменную. Поэтому каждый раз, когда вы можете использовать локальную переменную, имеет смысл использовать restrict.
@Barmar: я не говорил, что другой код должен использовать дополнительные значения size. Я сказал, например, что в это время в size пишет какой-то другой код, и вас это не волнует.
Ну, это было бы извращенно. Похоже, что это выходной параметр этой функции, никто больше не должен изменять его во время работы функции. Вы можете составлять теоретический код, но в практическом коде никогда не должно быть таких случаев.
@Barmar: О «… в практическом коде никогда не должно быть таких случаев»: ROTFL.
Возможно, но я был серьёзен. Если это место вывода одновременно изменяется из какого-либо другого кода, у вас есть ошибка. Две версии кода должны быть эквивалентны, за исключением оптимизации.
@Barmar Это простой пример. В более общем случае, вы бы выступали за void func(struct foo **dptr) { *dptr = malloc(sizeof(struct foo)); (*dptr)->x = 3; (*dptr)->y = 4; (*dptr)->z = 5; } вместо: void func(struct foo **dptr) { struct foo *sptr = malloc(sizeof(struct foo)); sptr->x = 3; sptr->y = 4; sptr->z = 5; *dptr = sptr; }
@CraigEstey Предлагаю void func(struct foo** restrict dptr) вместе с твоим первым телом.
@Barmar Итак, вы хотите сказать, что хотите, чтобы функция выполняла: (*dptr)->x et. ал. вместо sptr->x во всей функции??? Первое уродливо. С немного другой подписью: struct foo *func(void), использование sptr было бы очевидным. Использование (*dptr)->x не служит никакой цели во время выполнения, если dptr не модифицируется другим потоком и вы хотите такого поведения (чего, как вы сказали, не было). Вы говорите, что *dptr будет оптимизировано, так что на это надейтесь...
@CraigEstey Я рассматривал этот вопрос как вопрос о производительности, а не о стиле кодирования. В целом я сторонник кэширования выражений в переменных, чтобы избежать повторения сложных выражений. Но local_size на самом деле длиннее, чем (*size).
@Barmar ... Когда я исследую код, который *dptr используется повсюду в функции, мне приходится задаться вопросом: хотел ли первоначальный программист, чтобы другой поток изменил его? Итак, мне приходится просматривать множество кода, чтобы убедиться, что не используется нежелательное поведение (условия гонки и т. д.). Цель подхода sptr ясна: никаких побочных эффектов не требуется.
@Barmar Я знаю, что ты лучший программист, поэтому меня смутило то, что казалось «временным безумием» :-). Лично я бы переименовал переменные local_size --> size и size --> size_return, чтобы локальная переменная была короче параметра или глобальной. Кроме того, для отладки (с -g и -O0), используя sptr, компилятор [вероятно] генерирует лучше. Режим отладки и режим производства ближе (по скорости), поэтому (возможно) меньше фраз «мой код работает на -O0, но не на -O2»
С точки зрения оптимизации часто полезно создавать локальные копии небольших объектов памяти, поскольку они могут помочь компилятору решить проблемы с псевдонимами.
Компилятор, естественно, будет стараться максимально избегать доступа к памяти, кэшируя данные в регистрах, но иногда компилятору трудно решить, когда сделать недействительными эти кэшированные значения, поэтому ему приходится консервативно сбрасывать/перезагружать.
(Отчасти в этом можно помочь, опираясь на строгий псевдоним или квалификатор restrict, но локальные переменные обычно могут выполнять ту же самую работу).
Запись в ячейки памяти также означает, что даже если компилятору удастся использовать регистры для кэширования этих записей, ему может потребоваться очистить кеш раньше, чем если бы вы кэшировали вручную через локальную переменную, например, вызов непрозрачной функции или доступ. в память с неясным псевдонимом приведет к такой очистке.
Если сомневаетесь, сравните выходные данные сборки.
Первый пример — то, что я всегда делаю в подобной ситуации.
local_sizeмог бы жить в регистре ЦП и был бы быстрым. Во втором примере мы принудительно выполняем запись в память каждый раз, когда значение изменяется. В первом примере мы записываем эту память только один раз.