Re[4]: Насколько корректно использовать адрес переменной в стеке
От: watchmaker  
Дата: 26.07.17 12:20
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Возврат значения — это и есть немного подержать стек :)


К>Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться.

К>http://ideone.com/MTulkq
К>Я не сторонник такого трюкачества, но вдруг пригодится.

Честно говоря, если прочитать например SystemV ABI, то можно увидеть, что это автоматически происходит для всех структур (размер которых больше некоторого порогового значения).
То есть всё это извращение с передачей дефолтного параметра не нужно. Программисту достаточно написать просто
BigStruct foo() {
    BigStruct r;
    ...
    return r;
}

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



Единственный нюанс тут в том, что есть несколько разных ABI — формально нужно смотреть на версию своей для платформы. Но, к счастью, в вопросе об возврате структур по значению они практически все единогласны.
Re[5]: Насколько корректно использовать адрес переменной в стеке
От: Кодт Россия  
Дата: 26.07.17 12:52
Оценка: +1
Здравствуйте, watchmaker, Вы писали:

К>>Возврат значения — это и есть немного подержать стек

К>>Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться.
К>>Я не сторонник такого трюкачества, но вдруг пригодится.

W>Честно говоря, если прочитать например SystemV ABI, то можно увидеть, что это автоматически происходит для всех структур (размер которых больше некоторого порогового значения).

W>То есть всё это извращение с передачей дефолтного параметра не нужно.

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

W>Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.


Насчёт сугубо-вредности — тут можно пообсуждать.

Во-первых, мы не надеемся на NRVO, а вручную гарантируем это. И заодно, снимаем требование о копируемости-перемещаемости с типа. (Правда, добавляя некоторую дефолтную инициализацию и вторую фазу).
Во-вторых, с этим же трюком мы можем отдавать буфер произвольного вида. Ну самое простое,
#include <iostream>
#include <cstring>
using namespace std;

template<int N> struct charholder { char buf[N]; };

int main() {
  cout << strcat(strcat(strcpy(charholder<100>().buf,
                               "alfa"),
                        " + "),
                 "beta")
       << endl;
}

Здесь параметр не дефолтный, а очень даже явный, но суть та же.
Перекуём баги на фичи!
Re[6]: Насколько корректно использовать адрес переменной в с
От: watchmaker  
Дата: 26.07.17 13:17
Оценка:
Здравствуйте, Кодт, Вы писали:


W>>Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.


К>Насчёт сугубо-вредности — тут можно пообсуждать.


К>Во-первых, мы не надеемся на NRVO


Тут как бы дело в том, что такого поведения требует ABI, а не стандарт C++. Так делать компилятор обязан всегда, ибо программа должна удовлетворять требованиям из обоих этих документов.

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

То есть если рассматривать ситуацию с точки зрения только C++, то про конкретное устройство стека (и его взаимодействие с сигналами, прерываниями и прочем) практически ничего нельзя сказать, и поэтому нельзя утверждать, что такой код даст какой-то выигрыш.
Если же подключить знания об устройстве стека (которое описано в ABI), то выясняется, что такой код заведомо не нужен.
Вот этот последний ненужный код я и назвал вредным за то, что он просто сложнее при одновременном отсутствии какой-либо выгоды.

К> И заодно, снимаем требование о копируемости-перемещаемости с типа. (Правда, добавляя некоторую дефолтную инициализацию и вторую фазу).


Ага. Правда компилятор без этого трюка ещё и сам автоматически разрешит ситуацию, когда, например, у класса нет дефолтного инициализатора.
То есть там просто вместо готового объекта передаётся указатель на alignas(T) char buffer[sizeof(T)], в который потом можно сделать placement_new и вернуть указатель на новый объект.
Отредактировано 26.07.2017 14:51 watchmaker . Предыдущая версия .
Re[5]: Насколько корректно использовать адрес переменной в стеке
От: Nick-77  
Дата: 26.07.17 13:20
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Здравствуйте, Кодт, Вы писали:


К>>Возврат значения — это и есть немного подержать стек


К>>Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться.

К>>http://ideone.com/MTulkq
К>>Я не сторонник такого трюкачества, но вдруг пригодится.

W>Честно говоря, если прочитать например SystemV ABI, то можно увидеть, что это автоматически происходит для всех структур (размер которых больше некоторого порогового значения).

W>То есть всё это извращение с передачей дефолтного параметра не нужно. Программисту достаточно написать просто
BigStruct foo() {
W>    BigStruct r;
W>    ...
W>    return r;
W>}

W>И компилятор сам сделает замену: зарезервирует память в вызывающей функции и возвратит из функции указатель на r.
W>И это требование! То есть так поступать компилятор обязан даже при полностью выключенной оптимизации. И, конечное же, все компиляторы так и поступают: Пруф.
W>Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.


вот тут я перестал понимать, ведь это самое r и будет в стеке функции, который помрёт, т.е. от возврата указателя толку мало, или там будет какая-то неявная обёртка в memcpy?
Re[6]: Насколько корректно использовать адрес переменной в стеке
От: watchmaker  
Дата: 26.07.17 13:58
Оценка:
Здравствуйте, Nick-77, Вы писали:

N7>вот тут я перестал понимать, ведь это самое r и будет в стеке функции, который помрёт, т.е. от возврата указателя толку мало, или там будет какая-то неявная обёртка в memcpy?


Память под BigStruct будет выделена в стеке вызывающей функции. Поэтому эта память будет жить и после завершения вызываемой функции. И поэтому функция может вернуть указатель на эту память, так как этот указатель останется валидным даже после завершения foo.
И по этому указателю компилятор будет хранить r (на момент выхода из функции).
Уточнение в скобках важно.

То есть у функции foo есть участок в стеке, который заведомо переживёт время жизни самой foo, и в которой должна размещаться переменная r.

При этом, если переменная r размещается в этом месте всё время своей жизни, то это называется NRVO. Иначе же допускается, что переменная может быть сконструирована на собственном стеке foo, а потом перед выходом скопирована в указанную память, в том числе и через memcpy (эта ситуация называется "NRVO не сработал").

В общем-то Кодт очень хорошо написал выше вручную код, который компилятор сам делает. Можно сверится с ним.
Re[4]: Насколько корректно использовать адрес переменной в стеке
От: Pzz Россия https://github.com/alexpevzner
Дата: 26.07.17 14:12
Оценка:
Здравствуйте, cures, Вы писали:

C>Что мешает прерыванию случиться при работе программы в защищённом режиме? Вот если (асинхронные) прерывания запретили, то, наверное, шансов меньше. Но, во-первых, не уверен насчёт NMI, во-вторых, гипервизор вроде бы может прерывать даже выполнение в нулевом кольце, скажем, по сигналу с карты управления (отдельный Ethernet-интерфейс), и не факт, что он стек не затрёт. Ну и плюс крайне редкий вариант, что перед возвратом вдруг почему-то переполнилось TLB, соответственно, после возврата может произойти (синхронное) прерывание по неотмапленной странице (кода, или данных).


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

C>Так что такие возвращения — однозначный поиск неприятностей, для себя или для будущей поддержки. Если не платят зарплату — самое то


Это да.
Re: Насколько корректно использовать адрес переменной в стеке
От: Pzz Россия https://github.com/alexpevzner
Дата: 26.07.17 14:13
Оценка:
Здравствуйте, Nick-77, Вы писали:

N7>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.


Совсем никак не корректно.
Re[7]: Насколько корректно использовать адрес переменной в стеке
От: Nick-77  
Дата: 26.07.17 14:16
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Здравствуйте, Nick-77, Вы писали:


N7>>вот тут я перестал понимать, ведь это самое r и будет в стеке функции, который помрёт, т.е. от возврата указателя толку мало, или там будет какая-то неявная обёртка в memcpy?


W>Память под BigStruct будет выделена в стеке вызывающей функции. Поэтому эта память будет жить и после завершения вызываемой функции. И поэтому функция может вернуть указатель на эту память, так как этот указатель останется валидным даже после завершения foo.

W>И по этому указателю компилятор будет хранить r (на момент выхода из функции).
W>Уточнение в скобках важно.

W>То есть у функции foo есть участок в стеке, который заведомо переживёт время жизни самой foo, и в которой должна размещаться переменная r.


W>При этом, если переменная r размещается в этом месте всё время своей жизни, то это называется NRVO. Иначе же допускается, что переменная может быть сконструирована на собственном стеке foo, а потом перед выходом скопирована в указанную память, в том числе и через memcpy (эта ситуация называется "NRVO не сработал").


W>В общем-то Кодт очень хорошо написал выше вручную код, который компилятор сам делает. Можно сверится с ним.



тык у меня ж речь о голом С, а он разве подправляет заголовки функций, ведь получается, что допустим struct Mtype f(int a); должна быть преобразована НЕЯВНО в struct * Mtype f(Struct *val, int a);

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

?
Re[8]: Насколько корректно использовать адрес переменной в с
От: watchmaker  
Дата: 26.07.17 14:44
Оценка: 34 (1)
Здравствуйте, Nick-77, Вы писали:


N7>тык у меня ж речь о голом С, а он разве подправляет заголовки функций, ведь получается, что допустим struct Mtype f(int a); должна быть преобразована НЕЯВНО в struct * Mtype f(Struct *val, int a);


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


N7>?


Это правильный вопрос. Конкретный ответ на него даёт ABI. И обычно ответ: все должны делать эту замену (неявно, при совпадении некоторых условий).

И такая замена действительно происходит и при сборке C++, и при сборке голого C, и даже при сборке бинарных модулей Python.
Ведь, конечно, верно, что если бы это не происходило во всех языках одновременно и синхронно, то они бы не смогли корректно линковаться между собой.

Просто что касается того же Python, то внутри у него (у CPython) возврат структур сделан через собственную виртуальную машину и там вопрос про стек немного не имеет смысла. Но если нужно вызвать из Python какой-нибудь бинарный модуль, написанный на C, который при этом возвращает структуру, то такая замена произойдёт.
Но, конечно, в самом интерпретаторе Python об таких деталях не особо упоминают просто из-за того, что это немного offtopic. А Python делегирует эту низкоуровневую работу либо соответствующему компилятору (если собираем модуль на C, например), либо libffi (если подключаем модуль в run-time).
Отредактировано 26.07.2017 14:44 watchmaker . Предыдущая версия .
Re[7]: Насколько корректно использовать адрес переменной в стеке
От: Кодт Россия  
Дата: 26.07.17 14:52
Оценка: +1
Здравствуйте, watchmaker, Вы писали:

К>>Во-первых, мы не надеемся на NRVO

W>Тут как бы дело в том, что такого поведения требует ABI, а не стандарт C++. То есть NRVO тут вообще не при чём. Так делать компилятор обязан всегда, ибо программа должна удовлетворять требованиям из обоих этих документов.

ABI требует, чтобы вызывающая сторона передала буфер в функцию и получила туда ответ.
А будет ли это сделано задёшево или задорого, об этом ничего не сказано. И более того, — уже стандарт требует, чтобы компилятору разрешили делать это задорого (тип должен быть Copyable/Moveable), одновременно сам же разрешая компилятору делать это задёшево (NRVO).

К>> И заодно, снимаем требование о копируемости-перемещаемости с типа. (Правда, добавляя некоторую дефолтную инициализацию и вторую фазу).

W>Ага. Правда компилятор без этого трюка ещё и сам автоматически разрешит ситуацию, когда, например, у класса нет дефолтного инициализатора.

Ну уж тут "Направо пойдёшь — коня потеряешь, налево пойдёшь — сама тебя прибью. Твоя Василиса"

Конечно, рукосипедить нужно только тогда, когда исчерпаны обычные решения. То есть, если у нас жёсткие ограничения на свойства типа, или если профайлер нам здесь показал, что NRVO не выполняется, и что нам от этого плохо.
Перекуём баги на фичи!
Re[7]: Насколько корректно использовать адрес переменной в с
От: Кодт Россия  
Дата: 26.07.17 14:59
Оценка: +2
Здравствуйте, watchmaker, Вы писали:

W>То есть там просто вместо готового объекта передаётся указатель на alignas(T) char buffer[sizeof(T)], в который потом можно сделать placement_new и вернуть указатель на новый объект.


Кстати про aligned storage. (И, в частности, std::aligned_storage, чтобы не переизобретать).
Деструктор-то ведь тоже придётся вызывать вручную.

Более правильный подход — использовать в качестве буфера std::optional или его аналоги.
Перекуём баги на фичи!
Re[5]: Насколько корректно использовать адрес переменной в с
От: Лазар Бешкенадзе СССР  
Дата: 26.07.17 18:20
Оценка:
Здравствуйте, Pzz, Вы писали:

Pzz>В защищенных режимах, когда происходит прерывание, то обычно сначала аппаратура переключает стек на стек ядра, а потом уже в него гадит.


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

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

https://software.intel.com/sites/default/files/managed/a4/60/325384-sdm-vol-3abcd.pdf

6.12.1

• If the handler procedure is going to be executed at a numerically lower privilege level, a stack switch occurs.
When the stack switch occurs:
a. The segment selector and stack pointer for the stack to be used by the handler are obtained from the TSS for the currently executing task. On this new stack, the processor pushes the stack segment selector and stack pointer of the interrupted procedure.
b. The processor then saves the current state of the EFLAGS, CS, and EIP registers on the new stack (see Figures 6-4).
c. If an exception causes an error code to be saved, it is pushed on the new stack after the EIP value.
• If the handler procedure is going to be executed at the same privilege level as the interrupted procedure:
a. The processor saves the current state of the EFLAGS, CS, and EIP registers on the current stack (see Figures 6-4).
b. If an exception causes an error code to be saved, it is pushed on the current stack after the EIP value.

...

To return from an exception- or interrupt-handler procedure, the handler must use the IRET (or IRETD) instruction.
The IRET instruction is similar to the RET instruction except that it restores the saved flags into the EFLAGS register. The IOPL field of the EFLAGS register is restored only if the CPL is 0. The IF flag is changed only if the CPL is less than or equal to the IOPL. See Chapter 3, "Instruction Set Reference, A-L," of the Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 2A, for a description of the complete operation performed by the IRET instruction.
If a stack switch occurred when calling the handler procedure, the IRET instruction switches back to the interrupted procedure’s stack on the return.

Отредактировано 26.07.2017 19:07 Лазар Бешкенадзе . Предыдущая версия .
Re[2]: Насколько корректно использовать адрес переменной в стек
От: sts  
Дата: 02.09.17 21:32
Оценка:
Здравствуйте, Лазар Бешкенадзе, Вы писали:

ЛБ>Здравствуйте, Nick-77, Вы писали:


N7>>
N7>>static MType *f(MType *val, int size){
N7>>    MType tmp; 
N7>>    if (val)
N7>>        tmp = *val;
N7>>        else 
N7>>        // do smth to create new 
N7>>    ;
N7>>    // .....
N7>>    return &tmp;
N7>>}
N7>>


N7>>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.


ЛБ>Адрес то существует.


ЛБ>Я не знаю как сегодня но в 1998 это был отстой. Было понятие signal. Между выходом из функции и попыткой использовать содержимое по этому адресу может произойти туча всего и этот стек будет перезаписан 10 раз.
бред
Re[3]: Насколько корректно использовать адрес переменной в стек
От: Лазар Бешкенадзе СССР  
Дата: 03.09.17 15:27
Оценка:
Здравствуйте, sts, Вы писали:

sts>бред


Да уж. Дельфинарий.
Re: Насколько корректно использовать адрес переменной в стеке
От: Molchalnik  
Дата: 07.09.17 20:51
Оценка:
Здравствуйте, Nick-77, Вы писали:

N7>Вот в таком случае:



N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.



N7>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.

с вероятностью (90+x)% всё будет впорядке. Но с вероятностью (10 — x) % произойдут невероятные баги, которые невозможно будет отследить. И возникнут они не сразу, а через год существования проекта. И получится такой хтонический звиздец, что выть будет вся команда, сроки будут провалены, фирма получит убыток, а виновный кодер... В лучшем случае получит во все дырки, и ещё дополнительную просверлят в боку. А в худшем пойдёт доучивать материал для собеседований. А ещё компилятор может соптимизировать эту конструкцию неожиданным образом. Например, кланговский компилятор не стесняется даже волэйтил игнорировать в некоторых случаях.

Поэтому так никто не делает.
Re[2]: Насколько корректно использовать адрес переменной в стеке
От: swingus  
Дата: 08.09.17 01:35
Оценка: 5 (1)
То ли в 17м, то ли в 20 стандарте NRVO обязали делать. Теперь можно возвращать moveonly типы.

Здравствуйте, Кодт, Вы писали:


К>Если цель не в том, чтобы (контролируемо) выстрелить себе в ногу, а в том, чтобы вернуть значение по указателю, то

К>1) понадеяться на оптимизации NRVO
Re[3]: Насколько корректно использовать адрес переменной в стеке
От: N. I.  
Дата: 08.09.17 20:37
Оценка:
swingus:

S>То ли в 17м, то ли в 20 стандарте NRVO обязали делать.


Откуда инфа? В C++17 такого точно нет.

S>Теперь можно возвращать moveonly типы.


Уже в C++11 можно возвращать объекты классового типа, где нет вообще ни одного non-deleted конструктора; с введением в C++17 новых правил для prvalues появилось больше возможных вариантов сформировать валидное return statement. Но NRVO работает с lvalues, и к ней такие новшества никакого отношения не имеют.
Re[4]: Насколько корректно использовать адрес переменной в стеке
От: swingus  
Дата: 09.09.17 03:20
Оценка:
Здесь я вижу лёгкое безумие с вашей стороны. Если нет ни одного non-deleted конструктора (и оператора =), то это вероятно moveable type, а скорее всего copyable, всё зависит от членов класса. А взял я отсюда, например (на самом деле чуть ли не первая ссылка в выдаче):

The new C++17 standard brings many exciting new features. A smaller, more subtle improvement it brings is guaranteed copy elision. The keyword is guaranteed, as copy elision itself has always been part of the standard.

Здравствуйте, N. I., Вы писали:

NI>swingus:


S>>То ли в 17м, то ли в 20 стандарте NRVO обязали делать.


NI>Откуда инфа? В C++17 такого точно нет.


S>>Теперь можно возвращать moveonly типы.


NI>Уже в C++11 можно возвращать объекты классового типа, где нет вообще ни одного non-deleted конструктора; с введением в C++17 новых правил для prvalues появилось больше возможных вариантов сформировать валидное return statement. Но NRVO работает с lvalues, и к ней такие новшества никакого отношения не имеют.
Re[5]: Насколько корректно использовать адрес переменной в стеке
От: N. I.  
Дата: 09.09.17 06:24
Оценка:
swingus:

S>Здесь я вижу лёгкое безумие с вашей стороны. Если нет ни одного non-deleted конструктора (и оператора =), то это вероятно moveable type, а скорее всего copyable, всё зависит от членов класса.


Если нет ни одного non-deleted конструктора, то перемещать или копировать объект в контексте инициализации другого объекта такого же типа попросту нечем.

S>А взял я отсюда, например (на самом деле чуть ли не первая ссылка в выдаче):


S>The new C++17 standard brings many exciting new features. A smaller, more subtle improvement it brings is guaranteed copy elision. The keyword is guaranteed, as copy elision itself has always been part of the standard.


Про NRVO там сказано следующее:

A second consequence is that nothing changes for NVRO in C++17 with guaranteed copy elision. This is because, as mentioned before, the change only involves prvalues. With NVRO, the named value is a glvalue. The authors of the paper acknowledge this but chose to leave it out of scope.

Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.