Плохая производительность векторного <bool> в 64-битной цели с VS2012
Сравнительный анализ этого класса:
struct Sieve {
std::vector<bool> isPrime;
Sieve (int n = 1) {
isPrime.assign (n+1, true);
isPrime[0] = isPrime[1] = false;
for (int i = 2; i <= (int)sqrt((double)n); ++i)
if (isPrime[i])
for (int j = i*i; j <= n; j += i)
isPrime[j] = false;
}
};
Я получаю более 3-кратную худшую производительность (процессорное время) с 64-битной бинарной или 32-разрядной версией (выпускной сборкой) при вызове конструктора для большого числа, например.
Sieve s(100000000);
Я тестировал sizeof(bool)
, и для обеих версий это 1
.
Когда я заменяю vector<bool>
на vector<char>
, производительность становится одинаковой для 64-разрядных и 32-разрядных версий. Почему это?
Вот время выполнения для S(100000000)
(режим выпуска, 32-разрядный первый, 64-битный второй)):
vector<bool>
0,97 с 3,12 с
vector<char>
0,99 с 0,99 с
vector<int>
1,57 с 1,59 с
Я также провел тест на чувствительность с VS2010 (вызванный ответом Wouter Huysentruit), который произвел 0,98 с 0,88. Итак, что-то не так с реализацией VS2012.
Я отправил отчет об ошибке в Microsoft Connect
ИЗМЕНИТЬ
Многие ответы ниже содержат комментарии о недостатках использования int
для индексирования. Это может быть правдой, но даже сам Великий Волшебник использует стандартный for (int i = 0; i < v.size(); ++i)
в своих книгах, поэтому такой шаблон не должен нести существенного штрафа за производительность. Кроме того, эта проблема была поднята во время конференции Going Native 2013, и председательствующая группа гуру С++ прокомментировала их ранние рекомендации по использованию size_t
для индексирования и как возвращаемый тип size()
как ошибку. Они сказали: "Мы сожалеем, мы были молоды..."
Название этого вопроса может быть перефразировано до: Более 3-кратного снижения производительности этого кода при обновлении с VS2010 до VS2012.
ИЗМЕНИТЬ
Я сделал грубую попытку найти выравнивание памяти индексов i
и j
и обнаружил, что эта инструментальная версия:
struct Sieve {
vector<bool> isPrime;
Sieve (int n = 1) {
isPrime.assign (n+1, true);
isPrime[0] = isPrime[1] = false;
for (int i = 2; i <= sqrt((double)n); ++i) {
if (i == 17) cout << ((int)&i)%16 << endl;
if (isPrime[i])
for (int j = i*i; j <= n; j += i) {
if (j == 4) cout << ((int)&j)%16 << endl;
isPrime[j] = false;
}
}
}
};
автоматически запускается быстро (только на 10% медленнее, чем 32-разрядная версия). Это и производительность VS2010 затрудняют принятие теории оптимизатора, имеющей неотъемлемые проблемы с индексами int
вместо size_t
.
Ответы
Ответ 1
Я тестировал это с помощью vector<bool>
в VS2010: для 32-разрядных запросов требуется 1452 мс, тогда как для 64-разрядных бит требуется 1264 мс для i3.
Тот же тест в VS2012 (на i7 на этот раз) требует 700 мс (32-бит) и 2730 мс (64-разрядный), поэтому в компиляторе VS2012 есть что-то не так. Возможно, вы можете сообщить об этом в качестве ошибки в Microsoft.
UPDATE
Проблема заключается в том, что компилятор VS2012 использует временную переменную стека для части кода во внутреннем for-loop при использовании int в качестве итератора. Компоненты сборки, перечисленные ниже, являются частью кода внутри <vector>
, в += operator
std::vector<bool>::iterator
.
size_t как итератор
При использовании size_t
в качестве итератора часть кода выглядит следующим образом:
or rax, -1
sub rax, rdx
shr rax, 5
lea rax, QWORD PTR [rax*4+4]
sub r8, rax
Здесь все инструкции используют очень быстрые регистры CPU.
int как итератор
При использовании int
в качестве итератора эта же часть выглядит следующим образом:
or rcx, -1
sub rcx, r8
shr rcx, 5
shl rcx, 2
mov rax, -4
sub rax, rcx
mov rdx, QWORD PTR _Tmp$6[rsp]
add rdx, rax
Здесь вы видите используемую переменную стека _Tmp $6, что приводит к замедлению.
Компилятор точки в нужном направлении
Самое смешное, что вы можете указать компилятор в правильном направлении, используя vector<bool>::iterator
напрямую.
struct Sieve {
std::vector<bool> isPrime;
Sieve (int n = 1) {
isPrime.assign(n + 1, true);
std::vector<bool>::iterator it1 = isPrime.begin();
std::vector<bool>::iterator end = it1 + n;
*it1++ = false;
*it1++ = false;
for (int i = 2; i <= (int)sqrt((double)n); ++it1, ++i)
if (*it1)
for (std::vector<bool>::iterator it2 = isPrime.begin() + i * i; it2 <= end; it2 += i)
*it2 = false;
}
};
Ответ 2
std::vector<bool>
здесь не является прямой ошибкой. Разница в производительности в конечном итоге обусловлена использованием 32-битного типа int
в ваших циклах и некоторым довольно низким распределением регистров компилятором. Рассмотрим, например, ваш самый внутренний цикл:
for (int j = i*i; j <= n; j += i)
isPrime[j] = false;
Здесь j
- это 32-разрядное целое число со знаком. Однако, когда он используется в isPrime[j]
, его необходимо продвигать (и расширять с помощью знака) до 64-разрядного целого, чтобы выполнить вычисление индекса. Компилятор не может просто рассматривать j
как 64-битное значение, потому что это изменит поведение цикла (например, если n
является отрицательным). Компилятор также не может выполнить вычисление индекса, используя 32-разрядную величину j
, поскольку это изменит поведение этого выражения (например, если j
отрицательно).
Итак, компилятор должен сгенерировать код для цикла с использованием 32-разрядного j
, тогда он должен сгенерировать код для преобразования этого j
в 64-разрядное целое для вычисления индекса. Он должен сделать то же самое для внешнего цикла с помощью i
. К сожалению, похоже, что компилятор довольно плохо выделяет регистры в этом случае (*) - он начинает проливать временные ряды в стек, что приводит к тому, что производительность становится видимой.
Если вы измените свою программу на использование size_t
всюду (это 32-разрядная версия на x86 и 64-разрядной версии на x64), вы увидите, что производительность на одном уровне с x86, потому что сгенерированный код должен работать только с значения одного размера:
Sieve (size_t n = 1)
{
isPrime.assign (n+1, true);
isPrime[0] = isPrime[1] = false;
for (size_t i = 2; i <= static_cast<size_t>(sqrt((double)n)); ++i)
if (isPrime[i])
for (size_t j = i*i; j <= n; j += i)
isPrime[j] = false;
}
Вы все равно должны это делать, потому что смешивание подписанных и неподписанных типов, особенно когда эти типы имеют разную ширину, опасно и может привести к непредвиденным ошибкам.
Обратите внимание, что использование std::vector<char>
также "решает" проблему, но по другой причине: вычисление подстроек, необходимое для доступа к элементу a std::vector<char>
, существенно проще, чем для доступа к элементу std::vector<bool>
. Оптимизатор способен генерировать лучший код для более простых вычислений.
(*) Я не работаю над генерацией кода, и я вряд ли являюсь экспертом в сборе или оптимизации производительности на низком уровне, но, глядя на сгенерированный код, и учитывая, что здесь сообщается, что Visual С++ 2010 генерирует лучший код, я бы предположил, что есть возможности для улучшения в компиляторе. Я убеждаюсь, что обнаруженная вами ошибка подключения передается команде компилятора, чтобы они могли взглянуть.
Ответ 3
vector<bool>
- очень специальный контейнер, который специализируется на использовании 1 бит на элемент, а не на стандартную семантику контейнера. Я подозреваю, что логика манипуляции бит намного дороже при компиляции 64 бит (либо она по-прежнему использует 32-битные фрагменты для хранения бит, либо по какой-либо другой причине). vector<char>
ведет себя как обычный vector
, поэтому нет специальной логики.
Вы также можете использовать deque<bool>
, который не имеет специализации.