Достаточно ли ограничить только параметры «out» (указателя) функции?

Предположим, у меня есть функция, которая принимает некоторые параметры указателя — некоторые неконстантные, через которые она может писать, и некоторые константные, через которые она только читает. Пример:

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

но это не общая гарантия.

в этом тривиальном примере никакая дополнительная оптимизация невозможна, если вы объявляете указатель как restrict. Чего ты ожидал?

0___________ 05.09.2024 11:48

@0___________: Но если убрать все ограничения, пессимизация необходима. Добавил это в вопрос.

einpoklum 05.09.2024 12:57

restrict имеет значение только при изменении данных. Таким образом, модификация указателя данных ограниченным указателем a означает, что весь остальной доступ к этим данным должен осуществляться через a. Таким образом, любой доступ через указатель b никогда не может использовать псевдоним данных, на которые указывает a. Так что даже const для оптимизации больше не нужен и на практике никогда не нужен. См. godbolt.org/z/a6bGYvhsG

tstanisl 05.09.2024 13:34

@tstanisl: Да, я понимаю, что const для оптимизации не нужен. Я спрашивал, достаточно ли ограничения на выходы. Вы вроде бы говорите, что это так... можете подкрепить это цитатой из стандарта?

einpoklum 05.09.2024 13:43

@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 - Reinstate Monica 05.09.2024 13:54

@chux-ReinstateMonica: Я тоже так считаю, но я ищу что-то более официальное. P.S. Хочет ли Моника Челлио быть восстановлена ​​в должности на данный момент?

einpoklum 05.09.2024 13:56

@einpoklum Извините, я не могу предоставить информацию — глубокое погружение в ограничения отняло у меня много времени. Я просто придерживаюсь мнения, что restrict на a никак не помогает b иметь некоторые restrict свойства

chux - Reinstate Monica 05.09.2024 14:08
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
7
99
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ключевое слово const, как показано, вызовет предупреждение или ошибку, если вы попытаетесь изменить значение, на которое указывает указатель, но оно не обязательно помешает вам изменить это значение, и уж точно не помешает кому-то другому изменить это значение. Таким образом, компилятору, возможно, придется предположить, что такие изменения могут иметь место.

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

Более того, даже если вы измените значение, на которое указывает неконстантный указатель, этот неконстантный указатель может указывать на то же значение, что и константный указатель. (Например, f( &x, &x );) Без restrict в указателе const компилятор должен предполагать, что это тоже возможно.

Таким образом, даже параметр const может выиграть от ключевого слова restrict, поскольку оно обещает, что память, на которую указывает этот указатель, не будет никем изменена. По сути, вы обещаете, что никогда не будете делать что-то вроде f( &x, &x );.

Это утверждение недействительно. Если объект не volatile, вы обещаете, что указанный объект не будет изменен ничем при выполнении функции.

0___________ 05.09.2024 11:59

@0___________ Раньше я так же думала о volatile, но оказалось, что это не так. Если вы вызываете другую функцию, компилятор должен учитывать возможность того, что другая функция может изменить память, на которую указывает указатель const. Если вы измените значение, на которое указывает другой указатель, компилятор должен учитывать возможность того, что другой указатель может указывать на то же место, что и указатель const.

Mike Nakis 05.09.2024 12:12

"Я бы предположил" - ну это просто твоё мнение, чувак...

einpoklum 05.09.2024 12:41

«Ключевое слово const в указателе — это обязательное обещание, что вы не будете (не сможете) изменять память, на которую указывает указатель» — ложь. При квалификации типа, на который указывает, const выполняет только рекомендательную функцию: реализация C должна выдавать диагностику, если lvalue с const-квалифицированным является предметом присваивания или другой операции, которая его модифицирует (например, ++). Определение объекта как типа с указанием const означает, что реализация может предполагать, что он никогда не изменяется. Но тип, используемый для указания на объект, и тип, используемый для определения объекта, полностью независимы:…

Eric Postpischil 05.09.2024 14:20

… У вас могут быть все комбинации: указатель const на объект const, указатель const на объект, не являющийся const, указатель не-const на объект const и указатель не-const на объект, не являющийся const. Когда указатель `const` указывает на объект, отличный от const, он полностью определен, чтобы взять указатель на const, привести его к не-const и использовать его для изменения объекта. Это может произойти даже вне поля зрения компилятора, если указатель const передается другой функции, которая это делает.

Eric Postpischil 05.09.2024 14:20

@EricPostpischil, хорошо, так что бы я предпочел написать вместо «Ключевое слово const в указателе»? Должен ли я написать «Указатель на константу»?

Mike Nakis 05.09.2024 16:21

@MikeNakis: Похоже, вы неправильно поняли. Вопрос не в формулировке. int const *b не является обещанием, что функция не будет изменять память, на которую указывает b, и не является обещанием, что функция не будет изменять память, на которую b указывает, посредством использования b. Что такое constint const *b, так это гарантирует, что пользователь будет уведомлен диагностическим сообщением, если он использует *b (или связанные формы, такие как b[i]) имеет lvalue в контексте, где требуется изменяемое lvalue. Его функция — совет пользователю, а не обещание реализации.

Eric Postpischil 05.09.2024 16:33

Хорошо, тогда (и спасибо, что обсудили это со мной), как насчет этого: «Ключевое слово const, скорее всего, не позволит вам изменить память, на которую указывает указатель; однако...»

Mike Nakis 05.09.2024 16:38

Возможно, «const в int const *b заставит компилятор выдать вам предупреждение или ошибку, если вы попытаетесь изменить *b, но это не гарантирует, что компилятор *b не будет изменен…»

Eric Postpischil 05.09.2024 16:56
Ответ принят как подходящий

Нет, этого недостаточно.

Предположим также, что функция иначе не записывает в память (т. е. не использует глобальные переменные, фиксированные адреса, не преобразует константные указатели в неконстантные и тому подобные трюки).

Это предположение недостаточно. Также было бы необходимо, чтобы компилятор видел, что функция иначе не записывает в память, на которую указывает указатель (включая любую память, к которой можно получить доступ через указатель, например 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.

Хорошее замечание о том, что «предположение недостаточно».

chux - Reinstate Monica 05.09.2024 15:19

Предположим, у меня есть функция, которая принимает некоторые параметры указателя - некоторые неконстантный, с помощью которого он может писать, и некоторый константный, с помощью которого он может только читает. Пример:

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 используется для доступа к объекту, который он обозначает (чтение или запись), и
  • этот объект каким-либо образом изменяется во время выполнения функции, затем
  • каждый доступ к этому объекту (чтение или запись) во время выполнения этой функции будет осуществляться через lvalue, адрес которого «основан на» a, хотя и не обязательно именно через L.
  • (Например, к этому объекту нельзя будет получить доступ через значение lvalue «на основе» b, но не на a.)

Более подробно это изложено в разделе 6.7.3.1 C17, хотя этот текст одновременно сложен и немного перегружен.

Таким образом, в описанном вами случае restrictтолько a дает компилятору право предполагать, что чтение с помощью указателей, полученных из b, никогда не будет наблюдать за записью с помощью указателей, полученных из a. Разумеется, будет ли он на самом деле генерировать другой код — это совершенно другой вопрос.

«... дает компилятору право предполагать... из a» <- Но это не дает ответа на вопрос «Да» или «Нет» :-(

einpoklum 05.09.2024 21:07

@einpoklum, ведущий «restrict является односторонним. Он разрешает компилятору делать предположения [...], которые не зависят от того, является ли какой-либо другой указатель также restrict-квалифицированным» предназначен для более прямого ответа на вопрос позировал. Но моя проблема в том, что я не уверен, соответствуют ли ваши ожидания «преимуществ restrict» спецификации, поэтому вместо однозначного «да» я говорю вам, каких именно преимуществ вы можете ожидать.

John Bollinger 05.09.2024 21:17

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