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), щоб ви не ставили настільки високий попит на свою пам’ять.
NathanOliver

2
Просто пам'ять. Запускаються C ++ реалізації, які не використовують віртуальну пам'ять.
NathanOliver

3
Який компілятор, btw? Я не можу відтворити програму VS 2019 (16.4.2)
ChrisMM

3
З огляду на код libstdc ++, ця реалізація використовується лише у випадку, якщо тип елемента є тривіальним та присвоюється копією та якщо використовується за замовчуванням std::allocator.
волоський горіх

1
@Damon Як я вже згадував вище, він, мабуть, використовується лише для тривіальних типів з розподільником за замовчуванням, тому не повинно бути помітних різниць.
волоський горіх

Відповіді:


19

Немає обмежень у тому, скільки автоматичного зберігання використовує будь-який std 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), тому здається, що це не обмеження в мові, а скоріше лише в цій конкретній реалізації.


6
"не обов'язково вийде з ладу, незважаючи на переповнення стека, тому що виділена пам'ять ніколи не доступна програмою" - якщо стек буде використаний будь-яким чином після цього (наприклад, для виклику функції), це вийде з ладу навіть на платформах, що переборюють. .
Руслан

Будь-яка платформа, на якій це не виходить з ладу (якщо припустимо, що об'єкт не вдало виділений), є вразливою для Stack Clash.
користувач253751

@ user253751 Було б оптимістично вважати, що більшість платформ / програм не є вразливими.
eerorika

Я думаю, що перевиконання стосується лише купи, а не стека. Стек має фіксовану верхню межу за своїм розміром.
Джонатан Будкий

@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();

Остання версія не повинна створювати копію стека, а створювати величезний_тип безпосередньо в динамічній пам'яті вектора.

Я не можу авторитетно сказати, що те, що ви бачите, не відповідає, але це, безумовно, не те, що я очікував від якісної реалізації.


4
Як я вже згадував у коментарі до питання, libstdc ++ використовує цю реалізацію лише для тривіальних типів із призначенням копії std::allocator, тому не повинно бути помітної різниці між вставкою безпосередньо у пам'ять векторів та створенням проміжної копії.
волоський горіх

@walnut: Правильно, але величезний розподіл стеків та вплив init та копію на продуктивність - це все, що я не очікував від високоякісної реалізації.
Адріан Маккарті

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
dyp
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.