Как вышло, что наложение предполагается по умолчанию?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.09.21 09:23
Оценка:
Внезапно подумалось, почему оптимизаторы C/C++ традиционно исходят из возможности наложения (aliasing) указателей, позволяя явно указать его отсутствие (restrict, __declspec (noalias), __declspec (restrict) и т.п.), но явная возможность указать наличие наложения мало где есть? Соответственно, большинство компиляторов подходит к оптимизации достаточно осторожно, хотя можно было бы оптимизировать и лучше.

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

— Обсчет многомерных векторов/матриц, когда алгоритм предполагает разные выборки по размерностям, и это удобнее оформлять отдельными функциями.

— Формирование/преобразование изображений в [видео]памяти.

Но в подавляющем большинстве случаев наложения указателей не происходит, и осторожность явно лишняя.
compiler optimization aliasing pointer memory
Re: Как вышло, что наложение предполагается по умолчанию?
От: imh0  
Дата: 15.09.21 09:48
Оценка: -4
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>Внезапно подумалось, ....


Хороший вопрос, на самом деле!

ЕМ>Но в подавляющем большинстве случаев наложения указателей не происходит, и осторожность явно лишняя.


Думаю тут надо сначала разделить вопрос на СИ и С++, из-за того что в С++ есть ссылки, а в СИ нет.

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

В СИ же, нет ссылок, поэтому действительно оптимизация может быть хуже.

Я думаю, СИ является низкоуровневым языком, поэтому от него требуется быть более предсказуемым. Кроме того на программиста он наклыдывает требование более высокой квалификации. И поэтому с одной стороны "пусть не эффективно но предсказуемо", а с другой "пусть программист сам скажет, в узких местах, что нужна более сильная оптимизация".
Re: Как вышло, что наложение предполагается по умолчанию?
От: Zhendos  
Дата: 15.09.21 09:58
Оценка: +2
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>Внезапно подумалось, почему оптимизаторы C/C++ традиционно исходят из возможности наложения (aliasing) указателей, позволяя явно указать его отсутствие (restrict, __declspec (noalias), __declspec (restrict) и т.п.), но явная возможность указать наличие наложения мало где есть?


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

Ну и основная проблема конечно в том, что современные компиляторы
сами во многих случаях не могут докаать гипотезу что данные не перекрываются.
Re: Как вышло, что наложение предполагается по умолчанию?
От: Alexander G Украина  
Дата: 15.09.21 10:53
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>Внезапно подумалось, почему оптимизаторы C/C++ традиционно исходят из возможности наложения (aliasing) указателей, позволяя явно указать его отсутствие (restrict, __declspec (noalias), __declspec (restrict) и т.п.), но явная возможность указать наличие наложения мало где есть? Соответственно, большинство компиляторов подходит к оптимизации достаточно осторожно, хотя можно было бы оптимизировать и лучше.


Большинство -- это MSVC и мимикрирующий под него clang-cl?

Здесь видно, что только MSVC предполагает возможность наложения short* и int*, остальные только предполагают наложение int* и int* (icc и gcc при этом создаёт ветку без наложения, и определяет наложение).

Наличчие наложения указывается через __attribute((__may_alias__)), хотя это может быть слишком пессимистично, может хотеться указать, что на что может накладываться.
Русский военный корабль идёт ко дну!
Re[2]: Как вышло, что наложение предполагается по умолчанию?
От: Zhendos  
Дата: 15.09.21 11:02
Оценка: +1
Здравствуйте, imh0, Вы писали:

I>Здравствуйте, Евгений Музыченко, Вы писали:


I>Поэтому в С++ этот вопрос теряет актуальность.


Почему это?

Возьмите

int f(int &a, int &b)
{
   a += 1;
   b += 1;
   return a + b;
}

int f2(int * a, int *  b)
{
    *a += 1;
    *b += 1;
    return *a + *b;
}


в обоих случаях комплиятор в оптимизируещем режиме сгенерирует одинаковый код,
и так же будет предполагать что "a" и "b" являются "алиасами" на один и тот же адрес.
А вот если добавить во втором случае "int * __restrict", то компилятор сможет
использовать тот факт что "a" и "b" указывают на разный адрес и код будет разный между
кодом со ссылками и "restrict" указателями.

Ссылки в C++ никакого отношения к "__restrict" не имеют.
Отредактировано 15.09.2021 12:33 Zhendos . Предыдущая версия .
Re: Как вышло, что наложение предполагается по умолчанию?
От: rg45 СССР  
Дата: 15.09.21 11:52
Оценка: +2
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>Внезапно подумалось, почему оптимизаторы C/C++ традиционно исходят из возможности наложения (aliasing) указателей, позволяя явно указать его отсутствие (restrict, __declspec (noalias), __declspec (restrict) и т.п.), но явная возможность указать наличие наложения мало где есть?


Если нет уверенности, что наложение исключено, значит исходят из того, что наложение возможно. Корректность программы всегда была выше по приоритету, чем производительность. А как иначе?
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[3]: Как вышло, что наложение предполагается по умолчанию?
От: imh0  
Дата: 15.09.21 12:35
Оценка:
Здравствуйте, Zhendos, Вы писали:

I>>Поэтому в С++ этот вопрос теряет актуальность.


Z>Почему это?


Z>Возьмите

Z>

Z>int f(int &a, int &b)
Z>{
Z>   a += 1;
Z>   b += 1;
Z>   return a + b;
Z>}

Z>int f2(int * a, int *  b)
Z>{
Z>    *a += 1;
Z>    *b += 1;
Z>    return *a + *b;
Z>}
Z>



Даже не знаю, что тут сказать.... )

Ну давайте возьмем более осмысленный код, это как раз то, о чем я сообственно и говорил.

int f ( int & a,int &b )
{
    a+=1;
    b+=1;
    return a+b;
}

int f2 ( int * a )
{
    *(a+rand())+=1;
    *(a+rand())+=1;
    return *(a+rand()) + *(a+rand());
}

int main(int argc, char **argv) 
{
    int A1;
    int A2;
    int B2;
    int arr1[100];
    int arr2[100];
    int arr3[100];
    printf("f(A1,A1)=%d\n",f(A1,A1));
    printf("f(A2,B2)=%d\n",f(A2,B2));
    printf("f(arr1[10],arr1[10])=%d\n",f(arr1[10],arr1[10]));
    printf("f(arr2[10],arr2[11])=%d\n",f(arr2[10],arr2[11]));
    printf("f2(arr2[10])=%d\n",f2(&arr3[10]));
}


И посмотрим на код —

(gdb) disas main
Dump of assembler code for function main(int, char**):
=> 0x0000557e9c011080 <+0>: sub $0x198,%rsp
0x0000557e9c011087 <+7>: mov $0x4,%esi
0x0000557e9c01108c <+12>: lea 0xf71(%rip),%rdi # 0x557e9c012004
0x0000557e9c011093 <+19>: xor %eax,%eax
0x0000557e9c011095 <+21>: callq 0x557e9c011030 <printf@plt>
0x0000557e9c01109a <+26>: mov $0x2,%esi
0x0000557e9c01109f <+31>: lea 0xf6b(%rip),%rdi # 0x557e9c012011
0x0000557e9c0110a6 <+38>: xor %eax,%eax
0x0000557e9c0110a8 <+40>: callq 0x557e9c011030 <printf@plt>
0x0000557e9c0110ad <+45>: mov $0x4,%esi
0x0000557e9c0110b2 <+50>: lea 0xf65(%rip),%rdi # 0x557e9c01201e
0x0000557e9c0110b9 <+57>: xor %eax,%eax
0x0000557e9c0110bb <+59>: callq 0x557e9c011030 <printf@plt>
0x0000557e9c0110c0 <+64>: mov $0x2,%esi
0x0000557e9c0110c5 <+69>: lea 0xf6b(%rip),%rdi # 0x557e9c012037
0x0000557e9c0110cc <+76>: xor %eax,%eax
0x0000557e9c0110ce <+78>: callq 0x557e9c011030 <printf@plt>
0x0000557e9c0110d3 <+83>: lea 0x28(%rsp),%rdi
0x0000557e9c0110d8 <+88>: callq 0x557e9c011230 <f2(int*)>
0x0000557e9c0110dd <+93>: lea 0xf6c(%rip),%rdi # 0x557e9c012050
0x0000557e9c0110e4 <+100>: mov %eax,%esi
0x0000557e9c0110e6 <+102>: xor %eax,%eax
0x0000557e9c0110e8 <+104>: callq 0x557e9c011030 <printf@plt>
0x0000557e9c0110ed <+109>: xor %eax,%eax
0x0000557e9c0110ef <+111>: add $0x198,%rsp
0x0000557e9c0110f6 <+118>: retq 
End of assembler dump.
64^done


Что мы видим ?
1) Мы видим что компилятор прикрасно видит что идет ссылка на "одно и тоже"
2) Мы видим что по 0x0000557e9c0110d8 идет вызов f2. Причем понятно почему) Потому что мы МОЖЕМ заниматься адресной арифметикой с указателями, а со сылками какбы не особо)

Z>Ссылки в C++ никакого отношения к "__restrict" не имеют.


Что-то не туда уже куда-то поехало )
Re: Как вышло, что наложение предполагается по умолчанию?
От: watchmaker  
Дата: 15.09.21 12:35
Оценка: 5 (1) +1
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>Внезапно подумалось, почему оптимизаторы C/C++ традиционно исходят из возможности наложения (aliasing) указателей


Ты сам написал верное слово "традиционно".
Это и ответ — C++ не дизайнился не с нуля, а итеративно и на базе С (который не ISO C, а более старый и кривой). Который тоже был сделан не с нуля, а унаследовал концепции из ещё более низкоуровневых языков. Вплоть до ассемблера, в котором есть только указатели и сырая память, а никаких знаний об объектах и их семантики нет совсем. Отсюда и в С (который ещё не ISO C) тоже не было массивов с семантикой массивов, а были замаскированные под них указатели (из-за чего до сих пор конструкции array[index] и index[array] компилируются и выдают одинаковый результат).
Те же классы в С++ базируются на структурах С, которые базируются на смещениях от указателей. И вот опять потерялось знание, что два класса могут быть или не быть независимыми.

Если бы можно было переделать С++ с нуля, то в него можно было бы добавить десяток quality-of-life улучшений, которые бы не изменили принципиально язык, но немедленно бы сделали его более быстрым, простым и надёжным. Сюда не только вошли бы более разумные правила на aliasing, но и такие вещи как move для всех типов по умолчанию, менее безумные правила octal-literals (когда одна цифра в числе меняет систему счисления), быстрые строки (с длиной, а не null-terminated), отсутствие неявных преобразований (как литеральный 0 в указатель) и т.п.

Но всё это почти не могло случится эволюционным путём:
1. Сразу написать правильно сложно, особенно когда ты почти первопроходец и не знаешь с какими проблемами столкнётся язык через 10 лет;
2. Если делать правильно и переделать всё кардинально, то потеряется совместимость с предыдущим кодом, и такой язык сильно потеряет в привлекательности — кому нужен язык, если нельзя использовать многочисленные наработки, а нужно всё переписывать под него заново? Нужно очень сильно вложится, чтобы сделать такой язык популярным.

Где-то там Страуструп в "Дизайне и эволюции С++" об подобном рассказывал: либо ты делаешь С-with-classes и получаешь всё тяжкое наследие С и ассемблера, либо ты делаешь новый язык, который несовместим ни с чем, и поэтому у него не будет ни пользователей, ни ресурсов довести его до ума.
Re[2]: Как вышло, что наложение предполагается по умолчанию?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.09.21 13:27
Оценка:
Здравствуйте, imh0, Вы писали:

I>в С++ есть ссылки, а в СИ нет.


Речь не о синтаксисе, а семантически ссылка — тот же указатель. Имея какую-нибудь сложную структуру, содержащую в себе другие структуры, часто бывает удобно определить одну или несколько ссылок на эти подструктуры, чтобы не выписывать всю последовательность доступа. Или, имея двумерный массив, взять из него ссылку на одномерную строку.

Но подобное почти всегда происходит в рамках отдельной функции. Ситуации, когда функция получает указатель/ссылку на сложный объект в одном параметре, и на его подобъект — в другом, достаточно редки. Это или особые алгоритмы, или банальная криворукость.

I>Я думаю, СИ является низкоуровневым языком


С++ является почти столь же низкоуровневым языком, и это хорошо, это делает его уникальным и очень удобным. Некоторые не слишком умные люди предлагают оторвать от C++ низкоуровневые возможности, и двигать его исключительно в сторону высокого уровня, но они тупо не понимают сути языка, и очень хорошо, что их [пока] бьют по рукам.

I>поэтому от него требуется быть более предсказуемым.


Низкоуровневость ни разу не мешает предсказуемости. В случае неопределенного или платформ-зависимого поведения, дело компилятора — предупредить, и дать программисту возможность уточнить свою конструкцию. В этой области, чем меньше неконтролируемых предположений, тем лучше.
Re[2]: Как вышло, что наложение предполагается по умолчанию?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.09.21 13:31
Оценка:
Здравствуйте, Zhendos, Вы писали:

Z>если пользовать говорит что нафиг надежность, то он за это отвечает, что явно указано в коде.


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

Придумали же давным-давно volatile, хотя могли бы объявить: раз какие-то переменные могут меняться извне текущего потока, то и никаких вам регистровых оптимизаций, только память.
Re[2]: Как вышло, что наложение предполагается по умолчанию?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.09.21 13:33
Оценка:
Здравствуйте, Alexander G, Вы писали:

AG>Большинство -- это MSVC и мимикрирующий под него clang-cl?


Я ж не только о современных лидирующих — оптимизирующих компиляторов с C/C++ было достаточно. Кто, кроме GCC, давал возможность явно указывать возможность наложения?
Re[4]: Как вышло, что наложение предполагается по умолчанию?
От: Zhendos  
Дата: 15.09.21 14:11
Оценка: +2
Здравствуйте, imh0, Вы писали:

I>Даже не знаю, что тут сказать.... )


I>Ну давайте возьмем более осмысленный код, это как раз то, о чем я сообственно и говорил.


Я честно говоря тоже не знаю что сказать.
Здесь все вызовы будут встроенны, какое отношение приведенный
вами код вообще имеет к обсуждаемому вопросу?

I>Что мы видим ?


Мы видим что все заинлаилось, и все.

Давайте проще, допустим есть функция:
extern void f(Class &a, Class &b);


Я утверждаю что ее можно вызвать вот так "Class a; f(a, a);",
как я понимаю вы утверждаете, что компилятор для такого вызова выдаст ошибку?
Re[3]: Как вышло, что наложение предполагается по умолчанию?
От: Zhendos  
Дата: 15.09.21 14:24
Оценка: +1
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>Здравствуйте, Zhendos, Вы писали:


ЕМ>Если программист отвечает за бОльшую часть кода — зачем заставлять его явно указывать это везде? Он мог бы указать, что в общем случае предположений делать не нужно, но в некоторых местах это будет явно отменено.


ЕМ>Придумали же давным-давно volatile, хотя могли бы объявить: раз какие-то переменные могут меняться извне текущего потока, то и никаких вам регистровых оптимизаций, только память.


Ну "volatile" память относительно редкое явление (имеется ввиду правильный
вариант его применения, когда "volatile" испольуется для указания части
которая может меняться не программой).

А вот то, что в функцию типа
"void append(Vector &arg1, const Vector &arg2)" в конце концов
засунут одну и ту же переменную и в качестве первого аргумента и в качестве
второго вероятность этого очень большая.
Отредактировано 15.09.2021 14:31 Zhendos . Предыдущая версия .
Re[5]: Как вышло, что наложение предполагается по умолчанию?
От: imh0  
Дата: 15.09.21 14:39
Оценка: -1
Здравствуйте, Zhendos, Вы писали:

Z>Я честно говоря тоже не знаю что сказать.


Да просто ты рефлексируешь на знакомые слова. ) Попробуй подумать.

Z>Здесь все вызовы будут встроенны, какое отношение приведенный

Z>вами код вообще имеет к обсуждаемому вопросу?

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

I>>Что мы видим ?


Z>Мы видим что все заинлаилось, и все.


Нет. Мы видим, что для одинаковых и разных заинлайнилось по разному. То есть для ссылок, нет нужды париться, то есть для С++ проблема не стоит.

Z>Давайте проще, допустим есть функция:

Z>
Z>extern void f(Class &a, Class &b);
Z>


Твой extern нафиг никого не напугает. Будет можно и это заинлайнится. )
Уж коли хочешь что-то такое продемонстрировать пиши что-то типа — __attribute__((noinline))

Z>Я утверждаю что ее можно вызвать вот так "Class a; f(a, a);",

Z>как я понимаю вы утверждаете, что компилятор для такого вызова выдаст ошибку?

С чего бы вдруг? Ты написал компилятор обязан выполнить.
==
Еще раз — для С++ лучше использовать сылки и не использовать указатели. В этом случае работать будет также быстро, но компилятор сам разберется что там перекрывается а что нет.
Re[4]: Как вышло, что наложение предполагается по умолчанию?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.09.21 14:54
Оценка:
Здравствуйте, Zhendos, Вы писали:

Z>Ну "volatile" память относительно редкое явление (имеется ввиду правильный

Z>вариант его применения, когда "volatile" испольуется для указания части
Z>которая может меняться не программой).

А какая разница, меняется оно железом, или параллельным потоком самой программы? Если логика правильная, то volatile корректно работает везде. Другое дело, что для программных изменений эффективнее барьеры, чтобы компилятор не делал кода, читающего переменную при каждом доступе.
Re[6]: Как вышло, что наложение предполагается по умолчанию?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.09.21 14:58
Оценка: +2
Здравствуйте, imh0, Вы писали:

I>Еще раз — для С++ лучше использовать сылки и не использовать указатели. В этом случае работать будет также быстро, но компилятор сам разберется что там перекрывается а что нет.


Если и ссылки, и указатели создаются в одном модуле, он и с указателями нередко разберется. А если приходят снаружи, то никакой разницы.
Re[3]: Как вышло, что наложение предполагается по умолчанию?
От: B0FEE664  
Дата: 15.09.21 15:00
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:

Z>>если пользовать говорит что нафиг надежность, то он за это отвечает, что явно указано в коде.

ЕМ>Если программист отвечает за бОльшую часть кода — зачем заставлять его явно указывать это везде? Он мог бы указать, что в общем случае предположений делать не нужно, но в некоторых местах это будет явно отменено.

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

Правильно ли я понимаю, что под словом "некоторых" скрываются все стандартные алгоритмы из <algorithm>?

ЕМ>Придумали же давным-давно volatile, хотя могли бы объявить: раз какие-то переменные могут меняться извне текущего потока, то и никаких вам регистровых оптимизаций, только память.

Ага. А теперь volatile — deprecated. volatile имеет смысл только на однопроцессорной одноядерной архитектуре (что было давным давно), во всех остальных случаях это непонятно как работающая и что означающая конструкция.
И каждый день — без права на ошибку...
Re[5]: Как вышло, что наложение предполагается по умолчанию?
От: B0FEE664  
Дата: 15.09.21 15:06
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>А какая разница, меняется оно железом, или параллельным потоком самой программы? Если логика правильная, то volatile корректно работает везде. Другое дело, что для программных изменений эффективнее барьеры, чтобы компилятор не делал кода, читающего переменную при каждом доступе.


Это так, только если параллельный поток выполняется не параллельно, а последовательно. Для volatile нет гарантий атомарности.
И каждый день — без права на ошибку...
Re[4]: Как вышло, что наложение предполагается по умолчанию?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.09.21 15:36
Оценка:
Здравствуйте, B0FEE664, Вы писали:

BFE>если программист забыл отменить предположение об отмене наложения и получил очень странную, практически неуловимую ошибку, то его сразу призвать к ответу?


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

BFE>Правильно ли я понимаю, что под словом "некоторых" скрываются все стандартные алгоритмы из <algorithm>?


Положим, не все, но многие, да. Но много их лишь внутри <algorithm>, а в типичной программе — очень мало.

BFE>volatile имеет смысл только на однопроцессорной одноядерной архитектуре


Э-э-э... Когда железо меняет значение регистра в произвольные моменты времени — как это связано с количеством процессов/ядер, работающих с этим железом?
Re[6]: Как вышло, что наложение предполагается по умолчанию?
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.09.21 15:37
Оценка:
Здравствуйте, B0FEE664, Вы писали:

BFE>Для volatile нет гарантий атомарности.


А я что-то говорил за атомарность?
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.