Можно ли применить std::sort к std::unique‹T[ ]›?

Предположим, у меня есть динамический массив, который я хочу отсортировать, я мог бы сделать

std::vector<int> v(100);
for (int i = 0; i < 100; i++) v[i] = rand();
std::sort(v.begin(), v.end());

но для критически важного для производительности кода накладные расходы на инициализацию неприемлемы, подробнее см. https://stackoverflow.com/a/7269088/3667089

я тоже мог бы сделать

int *v = new int[100];
for (int i = 0; i < 100; i++) v[i] = rand();
std::sort(v, v + 100);

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

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

std::unique_ptr<int[]> v(new int[100]);
for (int i = 0; i < 100; i++) v[i] = rand();
std::sort(v, v + 100);

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

У меня Ubuntu 14.04, компилятор GCC.

РЕДАКТИРОВАТЬ: измените код, чтобы данные еще не были отсортированы


person user3667089    schedule 18.04.2016    source источник
comment
@KerrekSB Вау, это было быстро и правильно. Если вы напишете ответ, я приму его.   -  person user3667089    schedule 18.04.2016
comment
(Кстати, ваши данные уже отсортированы...)   -  person Kerrek SB    schedule 18.04.2016
comment
Сколько из этих предметов вы выделяете? Наличие пользовательского распределителя, который извлекает из большого заранее выделенного блока памяти, может решить ваши проблемы. Как примечание, версия GCC, которую вы используете, часто более актуальна, чем какая-либо ОС.   -  person tadman    schedule 18.04.2016
comment
Итак, настоящая проблема, которую вы пытаетесь решить, заключается в быстрой инициализации вектора?   -  person Mark Ransom    schedule 18.04.2016
comment
Я не уверен, что есть преимущество перед созданием пустого вектора и вызовом reserve(100). Также узким местом здесь может быть функция rand().   -  person Galik    schedule 18.04.2016
comment
Вы все еще используете компилятор 2010 года? Если нет, то ваши опасения, вероятно, не оправданы. Быстрый тест по связанному вопросу показывает, что useVector работает немного быстрее, чем useArray (с VC++ 2015), или с той же скоростью (с g++ 5.3).   -  person Jerry Coffin    schedule 18.04.2016
comment
@JerryCoffin Я пробовал с g++ 5.3, и использование необработанных массивов указателей по-прежнему намного быстрее, чем использование стандартного вектора.   -  person user3667089    schedule 18.04.2016
comment
Тогда вы почти наверняка делаете что-то не так (возможно, не включаете оптимизацию).   -  person Jerry Coffin    schedule 18.04.2016
comment
@JerryCoffin Вы упускаете важное различие между связанным вопросом (инициализация значения Pixel ничего не делает) и вопросом ОП (инициализация значения int выполняет нулевую инициализацию).   -  person Barry    schedule 18.04.2016
comment
@Barry: Если то, что он действительно хочет, это что-то, что действует как int, за исключением того, что инициализация значения - это nop, то это то, что он должен написать.   -  person Jerry Coffin    schedule 18.04.2016
comment
@JerryCoffin Это часть моего ответа. Но это не значит, что OP ошибается в оценке того, что векторный код медленнее, чем код массива.   -  person Barry    schedule 18.04.2016
comment
@Barry: Я не знаю - даже с таким кодом, как его, использующим int, я не могу найти никакой разницы, достаточно надежной, чтобы быть уверенным, что это реально. Быстрый тест с помощью gcc (например) показывает диапазон от 65 до 98 мс для массива и от 72 до 96 мс для вектора. Мне трудно оправдать исправление разницы, если так трудно даже быть уверенным, что она существует. Увеличение размера, похоже, тоже не помогает.   -  person Jerry Coffin    schedule 19.04.2016


Ответы (2)


std::sort по-прежнему нужны итераторы, а unique_ptr не является итератором. Однако он удерживает то, что можно использовать как единое целое: его указатель:

std::sort(v.get(), v.get() + 100);

or

std::sort(&*v, &*v + 100);

or

std::sort(&v[0], &v[0] + 100); // N.B. &v[100] invokes undefined behavior

Но вам на самом деле нужен vector распределитель, который инициализируется по умолчанию, а не по значению. Вот откуда возникает разница в производительности - использование std::vector с распределителем по умолчанию сначала инициализирует нулями все ваши int , а затем присваивает им некоторое значение, тогда как другие ваши варианты не имеют этого дополнительного шага нулевой инициализации.

Посмотрите на реализацию Casey такой вещи, а затем просто выполните:

std::vector<int, default_init_allocator<int>> v(100); // not zero-initialized
for (int i = 0; i < 100; i++) v[i] = i;
std::sort(v.begin(), v.end());

Другой подход, более простой в том смысле, что вам не нужно иметь дело с распределителями (хотя и более раздражающий с точки зрения кода), состоит в том, чтобы ввести оболочку для int, для которой инициализация значения ничего не делает:

template <class T>
struct Wrapper {
    Wrapper() { }
    T value;
};

std::vector<Wrapper<int>> v(100);              // not zero-initialized
for (int i = 0; i < 100; i++) v[i].value = i;  // but have to do this... 

Обратите внимание, что простое использование reserve() и push_back() недостаточно - это все еще немного больше работы, которую необходимо выполнить, чем простое назначение по индексу после инициализации по умолчанию, и если вы достаточно чувствительны к задержке, чтобы задать этот вопрос, это может быть значительным .

person Barry    schedule 18.04.2016
comment
Вариант 2 смешной. :) - person erip; 18.04.2016
comment
Вот небольшая демонстрация того, как неинициализирующийся распределитель влияет на codegen. - person Kerrek SB; 18.04.2016
comment
Примечание: как 2, так и 3 терпят неудачу, если *v имеет перегруженный operator &, поэтому не следует использовать в универсальном коде. Мораль: общий код написать невозможно. - person Yakk - Adam Nevraumont; 18.04.2016
comment
Я только что измерил время, скорость, необработанный указатель › вектор Кейси по умолчанию › unique_ptr › стандартный вектор, я не совсем понимаю, почему, потому что я ожидал, что необработанный указатель = вектор Кейси по умолчанию = unique_ptr › стандартный вектор ... - person user3667089; 18.04.2016
comment
Вариант 3 — УБ; это нарушает Requires на operator[]. - person T.C.; 18.04.2016
comment
@Т.С. Хм. Почему в vector::operator[] нет аналогичного пункта? Они делают то же самое для всех практических целей (для не vector<bool>). - person Barry; 18.04.2016
comment
Великолепный подробный ответ. Опечатка присваивания вместо ассинга. Кроме того, несмотря на применение ваших решений, чтобы избежать накладных расходов на инициализацию, необработанный указатель по-прежнему работает на ступеньку быстрее, и я нашел возможное объяснение на stackoverflow.com/ а/34596711/3667089. Похоже, что если производительность абсолютно критична, нет никакого способа избежать беспорядка с необработанными указателями... - person user3667089; 18.04.2016
comment
@Т.С. Ах, да. Так еще и УБ есть. Хотя на практике мне интересно, сколько существует кода, который выглядит так... - person Barry; 19.04.2016

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

std::vector<int> v;
v.reserve(100);
for (int i = 0; i < 100; i++) v.emplace_back(rand());
std::sort(v.begin(), v.end());
person Mark Ransom    schedule 18.04.2016
comment
Есть некоторые проверки, которые замедляют push_back, см. stackoverflow.com/a/20168172/3667089 - person user3667089; 18.04.2016
comment
@user3667089 user3667089, позвоните мне, когда вы сможете измерить влияние на производительность одной ветки (которая будет устранена путем прогнозирования ветвления на 4-й итерации) и неатомарного приращения. - person SergeyA; 18.04.2016
comment
@mark К сожалению, в std::vector нет метода emplace_back_I_know_there_is_capacity, и я еще не видел, чтобы компилятор понял это в любом тесте профилирования (или в любом выводе ASM). Может быть, вы докажете, что я ошибаюсь, я не искал по крайней мере год. - person Yakk - Adam Nevraumont; 18.04.2016
comment
@SergeyA Вы можете легко измерить влияние на производительность. Если бы вы написали бенчмарк, вы бы увидели существенную разницу. - person Barry; 18.04.2016
comment
@Barry, это может быть правдой, если инициализация тривиальна, например, константа или увеличивающееся значение. Разница быстро становится незначительной, если для создания значения выполняется какая-либо работа, например rand в вопросе. Да, это все еще может быть измеримо, и C++ по-прежнему позволяет вам писать на голое железо, если вам это нужно. Этот ответ просто пытается раздвинуть границы того, чего вы можете достичь без героических усилий. - person Mark Ransom; 18.04.2016
comment
@SergeyA Вот сообщение SO со ссылкой на тестовый код в реальном времени, где вы можете настроить его, чтобы увидеть замедление, вызванное push_back, по сравнению с доступом в стиле [] к тривиально конструируемому объекту. И да, rand() может быть дороже, чем накладные расходы от push_back/emplace_back. Я не перезапускал его в последнее время. - person Yakk - Adam Nevraumont; 18.04.2016
comment
@Yakk, я сделал. Тест производительности для 100 000 итераций, связанных с (массивом | вектором) из 100 000 элементов, дает мне 1 м 24,95 с пользовательского времени для вектора и 1 м 19,37 с пользовательского времени для массива. Я отказываюсь видеть, что это какая-то существенная разница, гарантирующая ручной массив, а не вектор. Код доступен по запросу. - person SergeyA; 18.04.2016
comment
@MarkRansom А? Разница одинакова в любом случае. Неважно, откуда берутся данные, время этой части постоянно, независимо от того, какой контейнер мы используем. - person Barry; 18.04.2016
comment
@Yakk, нет, рандинг. Как и в исходном вопросе. - person SergeyA; 18.04.2016
comment
@sergeyA Таким образом, 10 миллионов операций (100 тысяч итераторов из 100 тысяч элементов) на частоте 2 ГГц составляют ~ 200 циклов на элемент. Накладные расходы push_back составили около 12 циклов. Кажется разумным. Если вы работаете с эквивалентом изображения 4k (8 мегапикселей) с частотой 60 Гц (480 мегапикселей в секунду), push_back будет стоить 5,8 миллиарда циклов в секунду. По сути, этот уровень оптимизации имеет значение только тогда, когда вы приближаетесь к этой точке; но приблизиться к этой точке приблизиться несложно, не играя с нелепыми нагрузками. (Для тех, кто смотрит дома: не пытайтесь обрабатывать видео 4k в одном потоке процессора). - person Yakk - Adam Nevraumont; 18.04.2016
comment
@Yakk, на самом деле это 3 ГГц. model name : Intel(R) Xeon(R) CPU E5-2690 v2 @ 3.00GHz (надо было сказать об этом раньше) И, наверное, я понял вашу сферу деятельности? :) И то, как я это вижу, немного другое. Если то, что вы делаете, занимает 80 секунд, то вряд ли имеет значение, что это занимает 83 секунды. Возвращаясь к вашему примеру, я сомневаюсь, что для зрителей имеет значение, если один кадр визуализируется за 80 секунд, а не за 83 секунды, что по-прежнему неприемлемо. Вам действительно нужно быть на границе приемлемого, чтобы это имело хоть какое-то значение. - person SergeyA; 18.04.2016