Эффективность работы со ссылкой и локальной переменной в C

Я определил функцию в 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
}

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

Первый пример — то, что я всегда делаю в подобной ситуации. local_size мог бы жить в регистре ЦП и был бы быстрым. Во втором примере мы принудительно выполняем запись в память каждый раз, когда значение изменяется. В первом примере мы записываем эту память только один раз.

Craig Estey 23.02.2024 18:49

Вполне вероятно, что оптимизатор поймет, что *size можно кэшировать в регистре, поэтому обе версии будут создавать один и тот же код. @CraigEstey

Barmar 23.02.2024 18:50

@barmar Я использую флаг -O3 в компиляторе gcc, гарантирует ли это, что компилятор автоматически кеширует *size?

Cardstdani 23.02.2024 18:52

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

Barmar 23.02.2024 18:55

Возможно, объявление size_t *restrict size предотвратит это.

Barmar 23.02.2024 18:56

@Barmar Более подробно об этом можно узнать из недавнего вопроса: Почему GCC генерирует перемещение начала массива на каждой итерации цикла для доступа к массиву с помощью []? (-O3, x86) Я предпочитаю не предполагать, что оптимизатор «поступит правильно» в таких случаях. На мой взгляд, использование local_size — лучший/более понятный вариант с точки зрения производительности и ясности кода/намерений.

Craig Estey 23.02.2024 19:02
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
102
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Если //Function code... содержит что-либо, что может получить доступ к тому, на что указывает size, компилятор не сможет оптимизировать путем кэширования копии *size:

  • Если он использует идентификатор области файла для объекта, который может иметь псевдоним size_t, size может указывать на этот объект.
  • Если она вызывает любую другую функцию, определение которой компилятор не видит или не знает, эта функция может получить доступ к *size.
  • Если он получает указатель символа откуда-то, что не полностью известно компилятору, и использует этот указатель символа для изменения памяти, это может измениться *size, поскольку символьным типам разрешен доступ к любым объектам.
  • В вашем примере функции нет других параметров, но реальные функции часто имеют несколько параметров, которые вызывают одни и те же проблемы, поскольку они могут указывать на один и тот же объект size, на который указывает.

Итак, вам лучше использовать первую версию функции.

Будет ли квалификатор restrict сообщать компилятору, что подобные псевдонимы невозможны? Или речь идет только о последнем пункте?

Barmar 23.02.2024 19:09
restrict хотел бы обратиться к некоторым из них, верно?
ikegami 23.02.2024 19:09

@Barmar: Re: «Сообщит ли квалификатор restrict компилятору, что ни один из этих псевдонимов не может произойти?»: Да, но это то, что вы хотите сказать компилятору? На простом примере ОП, возможно, да. Но предположим, что что-то еще записывает в *size во время процедуры, но вас это не волнует, и вы просто хотите сохранить окончательный счетчик там, когда процедура будет завершена, даже если какая-то подпрограмма тем временем использовала его для временного хранения. Это достигается путем кэширования и сохранения в конце. Напротив, добавление restrict делает поведение программы неопределенным. В случаях, когда есть другие параметры,…

Eric Postpischil 23.02.2024 19:14

… можно учитывать и другие взаимодействия. Так что ответ на этот вопрос — не просто добавить restrict. Достаточно легко сохранить локальный счетчик, сохранить его в конце и сохранить код простым и понятным.

Eric Postpischil 23.02.2024 19:15

Если другому коду необходимо использовать инкрементальные значения size, вы не сможете кэшировать их в локальную переменную. Поэтому каждый раз, когда вы можете использовать локальную переменную, имеет смысл использовать restrict.

Barmar 23.02.2024 19:18

@Barmar: я не говорил, что другой код должен использовать дополнительные значения size. Я сказал, например, что в это время в size пишет какой-то другой код, и вас это не волнует.

Eric Postpischil 23.02.2024 19:20

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

Barmar 23.02.2024 19:24

@Barmar: О «… в практическом коде никогда не должно быть таких случаев»: ROTFL.

Eric Postpischil 23.02.2024 19:31

Возможно, но я был серьёзен. Если это место вывода одновременно изменяется из какого-либо другого кода, у вас есть ошибка. Две версии кода должны быть эквивалентны, за исключением оптимизации.

Barmar 23.02.2024 19:36

@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; }

Craig Estey 23.02.2024 21:26

@CraigEstey Предлагаю void func(struct foo** restrict dptr) вместе с твоим первым телом.

Barmar 23.02.2024 21:31

@Barmar Итак, вы хотите сказать, что хотите, чтобы функция выполняла: (*dptr)->x et. ал. вместо sptr->x во всей функции??? Первое уродливо. С немного другой подписью: struct foo *func(void), использование sptr было бы очевидным. Использование (*dptr)->x не служит никакой цели во время выполнения, если dptr не модифицируется другим потоком и вы хотите такого поведения (чего, как вы сказали, не было). Вы говорите, что *dptr будет оптимизировано, так что на это надейтесь...

Craig Estey 23.02.2024 21:54

@CraigEstey Я рассматривал этот вопрос как вопрос о производительности, а не о стиле кодирования. В целом я сторонник кэширования выражений в переменных, чтобы избежать повторения сложных выражений. Но local_size на самом деле длиннее, чем (*size).

Barmar 23.02.2024 21:57

@Barmar ... Когда я исследую код, который *dptr используется повсюду в функции, мне приходится задаться вопросом: хотел ли первоначальный программист, чтобы другой поток изменил его? Итак, мне приходится просматривать множество кода, чтобы убедиться, что не используется нежелательное поведение (условия гонки и т. д.). Цель подхода sptr ясна: никаких побочных эффектов не требуется.

Craig Estey 23.02.2024 21:59

@Barmar Я знаю, что ты лучший программист, поэтому меня смутило то, что казалось «временным безумием» :-). Лично я бы переименовал переменные local_size --> size и size --> size_return, чтобы локальная переменная была короче параметра или глобальной. Кроме того, для отладки (с -g и -O0), используя sptr, компилятор [вероятно] генерирует лучше. Режим отладки и режим производства ближе (по скорости), поэтому (возможно) меньше фраз «мой код работает на -O0, но не на -O2»

Craig Estey 23.02.2024 22:18

С точки зрения оптимизации часто полезно создавать локальные копии небольших объектов памяти, поскольку они могут помочь компилятору решить проблемы с псевдонимами.

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

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

Если сомневаетесь, сравните выходные данные сборки.

Другие вопросы по теме