Re[2]: Оптимизация использования стека под временные объекты
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 14.04.20 10:13
Оценка:
Здравствуйте, Sinclair, Вы писали:

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


Какой смысл раскладывать по разным адресам то, что никогда не доступно одновременно? Повторю: блок локальных переменных ничем не отличается от union'а, конфигурация которого определяется структурой блоков внутри функции. Вам приходилось слышать, чтобы реализация объединений хоть в компиляторе, хоть в отладчике, представляла собой заметную сложность?
Re[3]: Оптимизация использования стека под временные объекты
От: Sinclair Россия https://github.com/evilguest/
Дата: 14.04.20 10:46
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:
ЕМ>Какой смысл раскладывать по разным адресам то, что никогда не доступно одновременно? Повторю: блок локальных переменных ничем не отличается от union'а, конфигурация которого определяется структурой блоков внутри функции. Вам приходилось слышать, чтобы реализация объединений хоть в компиляторе, хоть в отладчике, представляла собой заметную сложность?
Дело не в сложности, а в постановке задачи. Когда вы делаете union, вы явно говорите компилятору о своём намерении совместить хранение.
А когда вы пишете что-то типа
for(int a=0; a<n; a++)
{
  ...
  if (d[a] > min_t)
    break;
}
int t = 42; //<== почему бы не использовать &a?

то выбор места хранения для a отдаёте компилятору. И при оптимизации, естественно, у него будет возможность повторно использовать место её хранения сразу после закрывающей }. Или вообще не хранить a.
А вот в режиме отладки вам может захотеться посмотреть на значение a после выхода из цикла. Совмещение адресов t и a, несмотря на формальную корректность, помешает этому вашему желанию.
Чтобы гарантировать отсутствие сюрпризов при отладке, компилятор перестаёт совмещать временные переменные. Более гранулярного контроля за этим у компилятора нет, увы.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[4]: Оптимизация использования стека под временные объекты
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 14.04.20 11:36
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>А вот в режиме отладки вам может захотеться посмотреть на значение a после выхода из цикла. Совмещение адресов t и a, несмотря на формальную корректность, помешает этому вашему желанию.

S>Чтобы гарантировать отсутствие сюрпризов при отладке, компилятор перестаёт совмещать временные переменные.

Все это здорово, да только, по факту, отладчики (что студийный, что WinDbg) как раз не дают возможности смотреть переменные, вышедшие из области видимости. Хотите сказать, что в компилятор умышленно встроена поддержка того, что столь же умышленно не поддерживается в отладчиках, и возможно только руками, на "сыром" стеке?

Ну и давайте еще зайдем с другой стороны. Вот программист пишет:

if (a > b) {
  double matrix [100][100];
  ...
} else {
  float sequence [20000];
  ...
}


И таких операторов в функции несколько штук. Мне кажется, что программист имеет основания предполагать минимальную вменяемость компилятора, и не рассматривать всерьез возможность исчерпания стека в такой ситуации. Однако ж, оно может наступить гораздо раньше, чем ожидалось.
Re[5]: Оптимизация использования стека под временные объекты
От: Sinclair Россия https://github.com/evilguest/
Дата: 15.04.20 05:38
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>Все это здорово, да только, по факту, отладчики (что студийный, что WinDbg) как раз не дают возможности смотреть переменные, вышедшие из области видимости. Хотите сказать, что в компилятор умышленно встроена поддержка того, что столь же умышленно не поддерживается в отладчиках, и возможно только руками, на "сыром" стеке?

Не совсем.
Вы правы — технически совмещение хранения переменных с непересекающимися scopes не повлияет на отладку. Но с точки зрения разработчиков компилятора, вот эти два случая эквивалентны:
int main()
{
    std::cout << "Hello World!\n";
    {
        int i = 42;
        std::cout << "i: " << i << "\n";
//    }{
        int j = 22;
        std::cout << "j: " << j << "\n";
    }
    std::cout << "Done.\n";
}
int main()
{
    std::cout << "Hello World!\n";
    {
        int i = 42;
        std::cout << "i: " << i << "\n";
    }{
        int j = 22;
        std::cout << "j: " << j << "\n";
    }
    std::cout << "Done.\n";
}

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

Впрочем, это только мои догадки — не знаю, есть ли в команде MSVC аналог Эрика Липперта, который бы расписывал детали принятия подобных решений.


ЕМ>Ну и давайте еще зайдем с другой стороны. Вот программист пишет:


ЕМ>
ЕМ>if (a > b) {
ЕМ>  double matrix [100][100];
ЕМ>  ...
ЕМ>} else {
ЕМ>  float sequence [20000];
ЕМ>  ...
ЕМ>}
ЕМ>


ЕМ>И таких операторов в функции несколько штук.

ЕМ>Мне кажется, что программист имеет основания предполагать минимальную вменяемость компилятора, и не рассматривать всерьез возможность исчерпания стека в такой ситуации.
Ну, ядерный стек исчерпается даже при совмещении данных, не так ли? А минимальная вменяемость компилятора включается при помощи флагов оптимизаций. Увы — так уж оно устроено.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: Оптимизация использования стека под временные объекты
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.04.20 06:03
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Неважно, как вы там расставили скобочки — стандарт языка определяет порядок событий; т.к. у примитивного типа деструктора нет, то причины продлевать время жизни i до выхода из блока нет.


Не понял, что Вы имеете в виду под "продлевать". Согласно стандарту, время жизни любого объекта, независимо от наличие деструктора, заканчивается при выходе из блока, в котором он определен. Разница между примитивным и классовым объектом будет исключительно в наличии вызова деструктора. Соответственно, при удалении "}{" время жизни i в любом случае продлевается до следующей закрывающей скобки.

S>Оптимизатор был бы плохим, если бы зависел от ручной разметки всех времён жизни.


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

S>Ну, ядерный стек исчерпается даже при совмещении данных, не так ли?


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

S>А минимальная вменяемость компилятора включается при помощи флагов оптимизаций.


И об этом тоже писал — у MS VC++ все это скопом включается/выключается флагом "global optimization". И ликвидация этой чудовищной избыточности автоматически делает недоступной для отладчика изрядную часть промежуточных переменных функции.
Re[7]: Оптимизация использования стека под временные объекты
От: cserg  
Дата: 15.04.20 07:40
Оценка: +1
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>... Согласно стандарту, время жизни любого объекта, независимо от наличие деструктора, заканчивается при выходе из блока, в котором он определен.

Распределение памяти под автоматтические переменные в стеке выполняет генератор кода, а он про стандарт C/C++ ничего не знает, мало того создается так, чтобы быть максимально независимым от исходного ЯП.

ЕМ>Разница между примитивным и классовым объектом будет исключительно в наличии вызова деструктора. Соответственно, при удалении "}{" время жизни i в любом случае продлевается до следующей закрывающей скобки.

Не в любом случае. Генератор кода определяет время жизни объекта по его использованию, а не по области видимости. Вызов деструктора — тоже использование, поэтому и продлевается время жизни. Оптимизатор проверяет время жизни объектов, если они не пересекаются, то он может распределить им одну область памяти.

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

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

ЕМ>а затраты на реализацию алгоритма были бы ничтожны.

Это ревлизация алгоритма для частного случая, поэтому и не делают.
Re[7]: Оптимизация использования стека под временные объекты
От: Sinclair Россия https://github.com/evilguest/
Дата: 15.04.20 07:58
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:
ЕМ>Не понял, что Вы имеете в виду под "продлевать". Согласно стандарту, время жизни любого объекта, независимо от наличие деструктора, заканчивается при выходе из блока, в котором он определен. Разница между примитивным и классовым объектом будет исключительно в наличии вызова деструктора. Соответственно, при удалении "}{" время жизни i в любом случае продлевается до следующей закрывающей скобки.
По стандарту — да. Но реально компилятор устраняет объект не после скобки, а когда может. В частности, при включенной агрессивной оптимизации в x86 режиме MSVC устраняет i как таковую — перед вызовом operator << в стек кладётся константа, и после вызова оператора никаких следов i не остаётся вообще.

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

Затраты на реализацию тут ни при чём. Уже реализован более дорогостоящий алгоритм с большим выигрышем. Реализовывать дополнительно к нему более убогий алгоритм посчитали излишним.

ЕМ>И об этом тоже писал — у MS VC++ все это скопом включается/выключается флагом "global optimization". И ликвидация этой чудовищной избыточности автоматически делает недоступной для отладчика изрядную часть промежуточных переменных функции.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re: Оптимизация использования стека под временные объекты
От: _NN_ www.nemerleweb.com
Дата: 15.04.20 14:18
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:

ЕМ>С неприятным удивлением обнаружил, что компилятор VC++, вплоть до самых последних версий, при отключенных оптимизациях совершенно не следит за выделением стека под объекты с разными областями видимости, которые можно было бы перекрыть в стековом кадре. Например, вот такая функция:


А какой компилятор так делает ?
Clang вон тоже не оптимизирует просто так: https://gcc.godbolt.org/z/R535Cp
Если нужна эта оптимизация, то установите соответствующий флаг оптимизации.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[8]: Оптимизация использования стека под временные объекты
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.04.20 15:49
Оценка:
Здравствуйте, cserg, Вы писали:

C>Распределение памяти под автоматтические переменные в стеке выполняет генератор кода, а он про стандарт C/C++ ничего не знает, мало того создается так, чтобы быть максимально независимым от исходного ЯП.


Какие есть основания полагать, что в MS в начале-середине 90-х делали кодогенератор с расчетом на языки, радикально отличные от C/C++ в плане использования стековой памяти?

C>Генератор кода определяет время жизни объекта по его использованию, а не по области видимости. Вызов деструктора — тоже использование, поэтому и продлевается время жизни.


Простите, я не понимаю смысла всех этих утверждений. Что Вы понимаете под "использованием"? Если это любое обращение к объекту, то чем вызов деструктора отличается от обращения из "языкового" кода? И то, и другое может происходить только внутри области видимости. Поэтому я не понимаю, что здесь означает "продлевается время жизни". Куда продлевается, зачем?

C>Оптимизатор проверяет время жизни объектов, если они не пересекаются, то он может распределить им одну область памяти.


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

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


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

C>Это ревлизация алгоритма для частного случая, поэтому и не делают.


Да там полно таких частных случаев. Как будто мы говорим об универсальном кодогенераторе для хотя бы трех принципиально разных языков.
Re[8]: Оптимизация использования стека под временные объекты
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.04.20 16:12
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>реально компилятор устраняет объект не после скобки, а когда может. В частности, при включенной агрессивной оптимизации в x86 режиме MSVC устраняет i как таковую


Это Вы сейчас к чему? Я же специально подчеркнул, что к работе оптимизатора претензий нет. А без глобальной оптимизации все раскладывается по стеку, и все деструкторы вызываются в концах блоков.

S>Уже реализован более дорогостоящий алгоритм с большим выигрышем. Реализовывать дополнительно к нему более убогий алгоритм посчитали излишним.


Если что, первые версии 32-разрядных компиляторов VC++ вообще не поддерживали оптимизации. В них не было реализовано "более дорогостоящего алгоритма".

Кстати, только что вспомнил, что у меня есть 16-разрядный компилятор VC++ 1.x (версия 08.00). Специально проверил — он при выключенных оптимизациях генерирует перекрывающуюся структуру стека. И отладчики там все прекрасно видят, и PDB в те времена уже были.
Re[2]: Оптимизация использования стека под временные объекты
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 15.04.20 16:36
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>А какой компилятор так делает ?


Например, 16-разрядный VC++ 1.x. Ну да, там даже в модели large особо не размахнешься, но выбрасывать уже имеющийся код, раскладывающий объекты по стеку, под лозунгом "ура, теперь у нас бесконечный стек!" было слегка неразумно.

_NN>Если нужна эта оптимизация, то установите соответствующий флаг оптимизации.


Интересно, какая часть ответивших удосужилась внимательно прочитать исходное сообщение?
Re[9]: Оптимизация использования стека под временные объекты
От: cserg  
Дата: 15.04.20 17:39
Оценка: +1
Здравствуйте, Евгений Музыченко, Вы писали:

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


ЕМ>Какие есть основания полагать, что в MS в начале-середине 90-х

Только предположение. К началу 90-х теория компиляции уже была достаточно зрелая.

ЕМ>делали кодогенератор с расчетом на языки, радикально отличные от C/C++ в плане использования стековой памяти?

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

C>>Генератор кода определяет время жизни объекта по его использованию, а не по области видимости. Вызов деструктора — тоже использование, поэтому и продлевается время жизни.


ЕМ>Простите, я не понимаю смысла всех этих утверждений. Что Вы понимаете под "использованием"?

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

ЕМ>Если это любое обращение к объекту, то чем вызов деструктора отличается от обращения из "языкового" кода?

Ничем, кроме того, что компилятор вставляет вызов деструктора вместо человека.

ЕМ>И то, и другое может происходить только внутри области видимости. Поэтому я не понимаю, что здесь означает "продлевается время жизни". Куда продлевается, зачем?

Время жизни не может превышать область видимости, но может быть меньше этой области.
void main()
{
    {
        A a;
        std::cout << "a: " << a << "\n";
        B b;
        std::cout << "b: " << b << "\n";
    }
}

Если у типа A нет деструктора, то время жизни a и b не пересекается, их можно разместить в одной области памяти. Если у А есть деструктор, то время жизни a будет продлено до конца области видимости и пересекать время жизни объекта b, их нельзя разместить в одной области памяти.

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

Ну, если вы выделяете оптимизатор, то он как раз располагается между анализатором и кодогенератором.

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

Правильно, зачем усложнять то, что и так сложно?
Re[3]: Оптимизация использования стека под временные объекты
От: _NN_ www.nemerleweb.com
Дата: 15.04.20 19:45
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:

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


_NN>>А какой компилятор так делает ?


ЕМ>Например, 16-разрядный VC++ 1.x. Ну да, там даже в модели large особо не размахнешься, но выбрасывать уже имеющийся код, раскладывающий объекты по стеку, под лозунгом "ура, теперь у нас бесконечный стек!" было слегка неразумно.

Видимо с течением времени решили , что это не нужно.

_NN>>Если нужна эта оптимизация, то установите соответствующий флаг оптимизации.


ЕМ>Интересно, какая часть ответивших удосужилась внимательно прочитать исходное сообщение?


Я читал внимательно и понял проблему.
Возможно есть какой-то конкретный флаг, который бы активировал только эту оптимизацию.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[10]: Оптимизация использования стека под временные объекты
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 16.04.20 03:34
Оценка:
Здравствуйте, cserg, Вы писали:

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


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

C>Время жизни не может превышать область видимости, но может быть меньше этой области.


Верно. Область видимости определяется сугубо синтаксически, а время жизни — более сложными методами. Именно поэтому фактическим временем жизни и занимается оптимизатор, а перекрывающееся распределение памяти вполне по силам базовому кодогенератору. Что мы и видим на примере VC++ 1.x (и, скорее всего, достаточного количества других компиляторов).

C>Если у типа A нет деструктора, то время жизни a и b не пересекается, их можно разместить в одной области памяти. Если у А есть деструктор, то время жизни a будет продлено до конца области видимости


Оно не "будет продлено", а "не будет сокращено без дополнительных условий". До конца области видимости оно распространяется по умолчанию.

C>и пересекать время жизни объекта b, их нельзя разместить в одной области памяти.


Этого никто и не требует. Прочитайте исходное сообщение.

C>Ну, если вы выделяете оптимизатор, то он как раз располагается между анализатором и кодогенератором.


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

C>Правильно, зачем усложнять то, что и так сложно?


Вопрос был бы уместным, если бы не было примеров реализаций описанного алгоритма в более ранних компиляторах. Скорее всего, от него отказались под влиянием "32-разрядной эйфории" 90-х, а уже потом поленились возвращать обратно.
Re[4]: Оптимизация использования стека под временные объекты
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 16.04.20 03:36
Оценка:
Здравствуйте, _NN_, Вы писали:

_NN>Возможно есть какой-то конкретный флаг, который бы активировал только эту оптимизацию.


У VC++ — нет, даже у последних. Или все, или ничего.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.