std :: vector (ab) использует автоматическое хранение

46

Рассмотрим следующий фрагмент:

#include <array>
int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  huge_type t;
}

Очевидно, что это приведет к сбою на большинстве платформ, поскольку размер стека по умолчанию обычно составляет менее 20 МБ.

Теперь рассмотрим следующий код:

#include <array>
#include <vector>

int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  std::vector<huge_type> v(1);
}

Удивительно, но и вылетает! Трассировка (с одной из последних версий libstdc ++) приводит к include/bits/stl_uninitialized.hфайлу, где мы можем видеть следующие строки:

typedef typename iterator_traits<_ForwardIterator>::value_type _ValueType;
std::fill(__first, __last, _ValueType());

Конструктор изменения размера vectorдолжен инициализировать элементы по умолчанию, и вот как это реализовано. Очевидно, _ValueType()временный сбой стека.

Вопрос в том, соответствует ли это реализации. Если да, то это на самом деле означает, что использование вектора огромных типов довольно ограничено, не так ли?

Игорь Р.
источник
Не следует хранить огромные объекты в виде массива. Это может потребовать очень большой области постоянной памяти, которая может отсутствовать. Вместо этого используйте вектор указателей (как правило, std :: unique_ptr), чтобы вы не предъявляли слишком большой спрос к вашей памяти.
Натан Оливер
2
Просто память. Существуют реализации C ++, которые не используют виртуальную память.
Натан Оливер
3
Какой компилятор, кстати? Я не могу воспроизвести с VS 2019 (16.4.2)
ChrisMM
3
Рассматривая код libstdc ++, эта реализация используется только в том случае, если тип элемента тривиален и может быть назначен для копирования, и если используется значение по умолчанию std::allocator.
грецкий орех
1
@ Damon Как я уже упоминал выше, кажется, что он используется только для тривиальных типов с распределителем по умолчанию, поэтому заметных отличий не должно быть.
грецкий орех

Ответы:

19

Нет ограничений на то, сколько автоматического хранилища использует любой стандартный API.

Всем им может потребоваться 12 терабайт стекового пространства.

Однако этот API требует только Cpp17DefaultInsertable, и ваша реализация создает дополнительный экземпляр над тем, что требуется конструктору. Если это не скрыто за обнаружением объекта, который легко можно копировать и копировать, эта реализация выглядит незаконной.

Якк - Адам Невраумонт
источник
8
Рассматривая код libstdc ++, эта реализация используется только в том случае, если тип элемента тривиален и может быть назначен для копирования, и если используется значение по умолчанию std::allocator. Я не уверен, почему этот особый случай сделан в первую очередь.
грецкий орех
3
@walnut Это означает, что компилятор может как бы не создавать этот временный объект; Я предполагаю, что есть шанс на оптимизированную сборку, которая не будет создана?
Якк - Адам Невраумонт
4
Да, я думаю, что это возможно, но для больших элементов GCC, похоже, нет. Clang с libstdc ++ действительно оптимизирует временную, но это кажется, только если размер вектора, переданный конструктору, является константой времени компиляции, см. Godbolt.org/z/-2ZDMm .
грецкий орех
1
@walnut есть особый случай, так что мы отправляем std::fillдля тривиальных типов, которые затем используют, memcpyчтобы разбить байты на места, что потенциально намного быстрее, чем создание множества отдельных объектов в цикле. Я считаю, что реализация libstdc ++ соответствует, но причиной переполнения стека для больших объектов является ошибка качества реализации (QoI). Я сообщил об этом как gcc.gnu.org/PR94540 и исправлю это.
Джонатан Уэйкли,
@JonathanWakely Да, это имеет смысл. Я не помню, почему я не думал об этом, когда писал свой комментарий. Думаю, я бы подумал, что первый элемент, созданный по умолчанию, будет создан непосредственно на месте, а затем его можно будет скопировать, так что никакие дополнительные объекты типа элемента никогда не будут созданы. Но, конечно, я не особо продумал это до мелочей, и я не знаю, как можно реализовать стандартную библиотеку. (Я понял слишком поздно, что это также ваше предложение в отчете об ошибке.)
грецкий орех
9
huge_type t;

Очевидно, что это приведет к сбою на большинстве платформ ...

Я оспариваю предположение о «большинстве». Поскольку память огромного объекта никогда не используется, компилятор может полностью ее игнорировать и никогда не выделять память, в этом случае не произойдет сбой.

Вопрос в том, соответствует ли это реализации.

Стандарт C ++ не ограничивает использование стека и даже не подтверждает существование стека. Итак, да, это соответствует стандарту. Но можно считать это вопросом качества реализации.

это на самом деле означает, что использование вектора огромных типов довольно ограничено, не так ли?

Это похоже на случай с libstdc ++. Сбой не был воспроизведен с помощью libc ++ (с использованием clang), поэтому кажется, что это не ограничение в языке, а скорее только в этой конкретной реализации.

eerorika
источник
6
«не обязательно будет аварийно завершать работу, несмотря на переполнение стека, поскольку программа никогда не обращается к выделенной памяти» - если после этого используется какой-либо стек (например, для вызова функции), это приведет к аварийному завершению даже на платформах с чрезмерной фиксацией ,
Руслан
Любая платформа, на которой это не дает сбоя (при условии, что объект не был успешно выделен) уязвима для Stack Clash.
user253751
@ user253751 Было бы оптимистичным предположить, что большинство платформ / программ не уязвимы.
eerorika
Я думаю, что overcommit относится только к куче, а не к стеку. Стек имеет фиксированную верхнюю границу своего размера.
Джонатан Уэйкли
@JonathanWakely Ты прав. Похоже, что причина этого не в том, что компилятор никогда не выделяет неиспользуемый объект.
eerorika
5

Я не адвокат по языкам и не эксперт по стандарту C ++, но cppreference.com говорит:

explicit vector( size_type count, const Allocator& alloc = Allocator() );

Создает контейнер с количеством вставленных по умолчанию экземпляров T. Копии не создаются.

Возможно, я неправильно понимаю «вставлено по умолчанию», но я ожидаю:

std::vector<huge_type> v(1);

быть эквивалентным

std::vector<huge_type> v;
v.emplace_back();

Последняя версия не должна создавать стековую копию, а должна создавать тип large_type непосредственно в динамической памяти вектора.

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

Адриан Маккарти
источник
4
Как я уже упоминал в комментарии к этому вопросу, libstdc ++ использует эту реализацию только для тривиальных типов с присваиванием копии и std::allocator, поэтому не должно быть заметной разницы между вставкой непосредственно в память векторов и созданием промежуточной копии.
грецкий орех
@walnut: Правильно, но огромное выделение стека и влияние init и copy на производительность - все еще вещи, которых я не ожидал бы от качественной реализации.
Адриан Маккарти
2
Да, я согласен. Я думаю, что это был недосмотр в реализации. Моя точка зрения заключалась лишь в том, что это не имеет значения с точки зрения соответствия стандартам.
грецкий орех
IIRC вам также нужно копирование или перемещение, emplace_backно не для создания вектора. Это означает, что вы можете иметь, vector<mutex> v(1)но не vector<mutex> v; v.emplace_back();для чего-то подобного, у huge_typeвас все еще может быть операция выделения и перемещения со второй версией. Никто не должен создавать временные объекты.
DYP
1
@IgorR. vector::vector(size_type, Allocator const&)требует (Cpp17) DefaultInsertable
дип