Предположим, у меня есть функция, которая принимает некоторые параметры указателя — некоторые неконстантные, через которые она может писать, и некоторые константные, через которые она только читает. Пример:
void f(int * a, int const *b);
Предположим также, что функция иначе не записывает в память (т. е. не использует глобальные переменные, фиксированные адреса, не преобразует константные указатели в неконстантные и тому подобные трюки).
Теперь достаточно ли (согласно стандарту языка C) для достижения преимуществ restrict
для всех операций чтения внутри f()
только restrict
выходные параметры? то есть в примере ограничить a
, но не b
?
Простой тест (GodBolt) показывает, что такого ограничения должно быть достаточно. Этот источник:
int f(int * restrict a, int const * b) {
a[0] += b[0];
return a[0] + b[0];
}
int all_restricted(int * restrict a, int const * restrict b) {
a[0] += b[0];
return a[0] + b[0];
}
int unrestricted(int * a, int const * b) {
a[0] += b[0];
return a[0] + b[0];
}
Создает тот же объектный код для x86_64:
f:
mov eax, DWORD PTR [rsi]
mov edx, DWORD PTR [rdi]
add edx, eax
mov DWORD PTR [rdi], edx
add eax, edx
ret
all_restricted:
mov eax, DWORD PTR [rsi]
mov edx, DWORD PTR [rdi]
add edx, eax
mov DWORD PTR [rdi], edx
add eax, edx
ret
unrestricted:
mov eax, DWORD PTR [rsi]
add eax, DWORD PTR [rdi]
mov DWORD PTR [rdi], eax
add eax, DWORD PTR [rsi]
ret
но это не общая гарантия.
@0___________: Но если убрать все ограничения, пессимизация необходима. Добавил это в вопрос.
restrict
имеет значение только при изменении данных. Таким образом, модификация указателя данных ограниченным указателем a
означает, что весь остальной доступ к этим данным должен осуществляться через a
. Таким образом, любой доступ через указатель b
никогда не может использовать псевдоним данных, на которые указывает a
. Так что даже const
для оптимизации больше не нужен и на практике никогда не нужен. См. godbolt.org/z/a6bGYvhsG
@tstanisl: Да, я понимаю, что const
для оптимизации не нужен. Я спрашивал, достаточно ли ограничения на выходы. Вы вроде бы говорите, что это так... можете подкрепить это цитатой из стандарта?
@einpoklum Насколько я понимаю: 1) int * restrict a
подразумевает, что указатель a
, заданный функции, не перекрывает другие ссылочные данные. Все изменения *a
видны непосредственно в функции. int const * b
, без restrict
не имеет этой гарантии для *b
. const
подразумевает, что функция не будет напрямую менять *b
, но меняет *a
, как в случае a[0] += b[0];
все еще может изменить *b
через f(p, p);
2) Опубликованная сборка является индикатором, но недостаточным. Неправильно вызванные функции, которые предотвращают небезопасный аргумент, приводят к UB. Выдаваемый код не обязательно должен быть другим.
@chux-ReinstateMonica: Я тоже так считаю, но я ищу что-то более официальное. P.S. Хочет ли Моника Челлио быть восстановлена в должности на данный момент?
@einpoklum Извините, я не могу предоставить информацию — глубокое погружение в ограничения отняло у меня много времени. Я просто придерживаюсь мнения, что restrict
на a
никак не помогает b
иметь некоторые restrict
свойства
Ключевое слово const
, как показано, вызовет предупреждение или ошибку, если вы попытаетесь изменить значение, на которое указывает указатель, но оно не обязательно помешает вам изменить это значение, и уж точно не помешает кому-то другому изменить это значение. Таким образом, компилятору, возможно, придется предположить, что такие изменения могут иметь место.
Например, если вы вызываете некоторую функцию, определенную в другом исходном файле, компилятор не будет знать, что делает эта функция, поэтому ему придется предположить, что функция каким-то образом может иметь доступ к значению, указанному этим указателем, и может измените его.
Более того, даже если вы измените значение, на которое указывает неконстантный указатель, этот неконстантный указатель может указывать на то же значение, что и константный указатель. (Например, f( &x, &x );
) Без restrict
в указателе const компилятор должен предполагать, что это тоже возможно.
Таким образом, даже параметр const
может выиграть от ключевого слова restrict
, поскольку оно обещает, что память, на которую указывает этот указатель, не будет никем изменена. По сути, вы обещаете, что никогда не будете делать что-то вроде f( &x, &x );
.
Это утверждение недействительно. Если объект не volatile
, вы обещаете, что указанный объект не будет изменен ничем при выполнении функции.
@0___________ Раньше я так же думала о volatile
, но оказалось, что это не так. Если вы вызываете другую функцию, компилятор должен учитывать возможность того, что другая функция может изменить память, на которую указывает указатель const
. Если вы измените значение, на которое указывает другой указатель, компилятор должен учитывать возможность того, что другой указатель может указывать на то же место, что и указатель const
.
"Я бы предположил" - ну это просто твоё мнение, чувак...
«Ключевое слово const
в указателе — это обязательное обещание, что вы не будете (не сможете) изменять память, на которую указывает указатель» — ложь. При квалификации типа, на который указывает, const
выполняет только рекомендательную функцию: реализация C должна выдавать диагностику, если lvalue с const
-квалифицированным является предметом присваивания или другой операции, которая его модифицирует (например, ++
). Определение объекта как типа с указанием const
означает, что реализация может предполагать, что он никогда не изменяется. Но тип, используемый для указания на объект, и тип, используемый для определения объекта, полностью независимы:…
… У вас могут быть все комбинации: указатель const
на объект const
, указатель const
на объект, не являющийся const
, указатель не-const
на объект const
и указатель не-const
на объект, не являющийся const
. Когда указатель `const` указывает на объект, отличный от const
, он полностью определен, чтобы взять указатель на const
, привести его к не-const
и использовать его для изменения объекта. Это может произойти даже вне поля зрения компилятора, если указатель const
передается другой функции, которая это делает.
@EricPostpischil, хорошо, так что бы я предпочел написать вместо «Ключевое слово const в указателе»? Должен ли я написать «Указатель на константу»?
@MikeNakis: Похоже, вы неправильно поняли. Вопрос не в формулировке. int const *b
не является обещанием, что функция не будет изменять память, на которую указывает b
, и не является обещанием, что функция не будет изменять память, на которую b
указывает, посредством использования b
. Что такое const
int const *b
, так это гарантирует, что пользователь будет уведомлен диагностическим сообщением, если он использует *b
(или связанные формы, такие как b[i]
) имеет lvalue в контексте, где требуется изменяемое lvalue. Его функция — совет пользователю, а не обещание реализации.
Хорошо, тогда (и спасибо, что обсудили это со мной), как насчет этого: «Ключевое слово const, скорее всего, не позволит вам изменить память, на которую указывает указатель; однако...»
Возможно, «const
в int const *b
заставит компилятор выдать вам предупреждение или ошибку, если вы попытаетесь изменить *b
, но это не гарантирует, что компилятор *b
не будет изменен…»
Нет, этого недостаточно.
Предположим также, что функция иначе не записывает в память (т. е. не использует глобальные переменные, фиксированные адреса, не преобразует константные указатели в неконстантные и тому подобные трюки).
Это предположение недостаточно. Также было бы необходимо, чтобы компилятор видел, что функция иначе не записывает в память, на которую указывает указатель (включая любую память, к которой можно получить доступ через указатель, например b[18]
). Например, если вызывается bar(b);
, а компилятор не видит bar
, то он не может знать, изменяется ли память, на которую указывает b
, во время выполнения f
, даже если это не так.
Дайте эту дополнительную предпосылку, что компилятор может видеть, что нет никаких изменений в любой памяти, на которую указывает b
, тогда для оптимизации не имеет значения, объявлен ли b
с помощью const
и/или restrict
: компилятор знает все о памяти и сообщает это ничего больше информации не добавляет.
Однако зачастую код не удовлетворяет этому предположению. (И даже если это так, программисту может быть неприятно быть в этом уверенным.) Итак, давайте рассмотрим случай, когда у нас нет дополнительной посылки:
void f(int * restrict a, int const *b)
{
printf("%d\n", *b);
bar();
printf("%d\n", *b);
}
Когда вызывается bar
, компилятор не знает, изменен ли *b
. Несмотря на то, что эта функция не передает b
в bar
, bar
может получить доступ к некоторому внешнему объекту, который является *b
или имеет указатель на то, где находится *b
, и поэтому bar
может изменить объект, который является *b
. Поэтому компилятор должен перезагрузить *b
из памяти для второго printf
.
Если вместо этого мы объявим функцию void f(int * restrict a, int const * restrict b)
, то restrict
утверждает, что если *b
изменяется во время выполнения f
(в том числе косвенно, внутри bar
), то каждый доступ к ней будет осуществляться через b
(напрямую, как в *b
, или косвенно, как через указатель, явно скопированный или вычисленный из b
). Поскольку компилятор видит bar
не получает b
, он знает, что bar
не содержит никаких обращений к *b
, основанных на b
, и поэтому может предположить, что bar
не изменяется *b
.
Таким образом, добавление restrict
к параметру, который является указателем на тип с const
, может обеспечить некоторую оптимизацию, даже если все остальные параметры также объявлены restrict
.
Хорошее замечание о том, что «предположение недостаточно».
Предположим, у меня есть функция, которая принимает некоторые параметры указателя - некоторые неконстантный, с помощью которого он может писать, и некоторый константный, с помощью которого он может только читает. Пример:
void f(int * a, int const *b);
Предположим также, что функция иначе не записывает в память (т.е. не использует глобальные переменные, фиксированные адреса, перестановку константные указатели как неконстантные и тому подобные трюки).
Теперь достаточно ли этого (согласно стандарту языка C) для достижения преимущества
restrict
для всех прочтений в пределахf()
, только дляrestrict
выходные параметры? то есть в примере ограничитьa
, но неb
?
restrict
односторонний. Он разрешает компилятору делать предположения о доступе к объекту через lvalue, «основанные» на restrict
-квалифицированном указателе, которые не зависят от того, является ли какой-либо другой указатель также restrict
-квалифицированным.
В частности, если вы restrict
параметр a
, то независимо от того, используете ли вы также restrict
параметр b
, вы разрешаете компилятору предполагать, что во время любого выполнения f()
,
L
— любое lvalue, адрес которого «основан на» a
, иL
используется для доступа к объекту, который он обозначает (чтение или запись), иa
, хотя и не обязательно именно через L
.b
, но не на a
.)Более подробно это изложено в разделе 6.7.3.1 C17, хотя этот текст одновременно сложен и немного перегружен.
Таким образом, в описанном вами случае restrict
только a
дает компилятору право предполагать, что чтение с помощью указателей, полученных из b
, никогда не будет наблюдать за записью с помощью указателей, полученных из a
. Разумеется, будет ли он на самом деле генерировать другой код — это совершенно другой вопрос.
«... дает компилятору право предполагать... из a» <- Но это не дает ответа на вопрос «Да» или «Нет» :-(
@einpoklum, ведущий «restrict
является односторонним. Он разрешает компилятору делать предположения [...], которые не зависят от того, является ли какой-либо другой указатель также restrict
-квалифицированным» предназначен для более прямого ответа на вопрос позировал. Но моя проблема в том, что я не уверен, соответствуют ли ваши ожидания «преимуществ restrict
» спецификации, поэтому вместо однозначного «да» я говорю вам, каких именно преимуществ вы можете ожидать.
в этом тривиальном примере никакая дополнительная оптимизация невозможна, если вы объявляете указатель как
restrict
. Чего ты ожидал?