Поиск STL лучше, чем ручной цикл

У меня есть вопрос. Учитывая следующий фрагмент кода С++:

#include <boost/progress.hpp>

#include <vector>
#include <algorithm>
#include <numeric>
#include <iostream>

struct incrementor
{
  incrementor() : curr_() {}

  unsigned int operator()()
  { return curr_++; }

private:
  unsigned int curr_;
};

template<class Vec>
char const* value_found(Vec const& v, typename Vec::const_iterator i)
{
  return i==v.end() ? "no" : "yes";
}


template<class Vec>
typename Vec::const_iterator find1(Vec const& v, typename Vec::value_type val)
{
  return find(v.begin(), v.end(), val);
}


template<class Vec>
typename Vec::const_iterator find2(Vec const& v, typename Vec::value_type val)
{
  for(typename Vec::const_iterator i=v.begin(), end=v.end(); i<end; ++i)
    if(*i==val) return i;
  return v.end();
}

int main()
{
  using namespace std;
  typedef vector<unsigned int>::const_iterator iter;
  vector<unsigned int> vec;
  vec.reserve(10000000);

  boost::progress_timer pt;

  generate_n(back_inserter(vec), vec.capacity(), incrementor());
  //added this line, to avoid any doubts, that compiler is able to
  // guess the data is sorted
  random_shuffle(vec.begin(), vec.end());

  cout << "value generation required: " << pt.elapsed() << endl;

  double d;
  pt.restart();
  iter found=find1(vec, vec.capacity());
  d=pt.elapsed();
  cout << "first search required: " << d << endl;
  cout << "first search found value: " << value_found(vec, found)<< endl;


  pt.restart();
  found=find2(vec, vec.capacity());
  d=pt.elapsed();
  cout << "second search required: " << d << endl;
  cout << "second search found value: " << value_found(vec, found)<< endl;


  return 0;
}

На моей машине (Intel i7, Windows Vista) STL find (вызов через find1) работает примерно в 10 раз быстрее, чем ручной цикл (вызов через find2). Сначала я подумал, что Visual С++ выполняет какую-то вектологию (может быть, я ошибаюсь здесь), но насколько я вижу, сборка не выглядит так, как она использует векторию. Почему цикл STL быстрее? Ручная петля идентична петле из тела STL-find.

Мне было предложено опубликовать выпуск программы. Без тасования:

value generation required: 0.078
first search required: 0.008
first search found value: no
second search required: 0.098
second search found value: no

С тасованием (эффекты кеширования):

value generation required: 1.454
first search required: 0.009
first search found value: no
second search required: 0.044
second search found value: no

Большое спасибо,

Dusha.

P.S. Я возвращаю итератор и выписываю результат (найденный или нет), потому что я хотел бы предотвратить оптимизацию компилятора, что он считает, что цикл не требуется вообще. Выбранное значение, очевидно, не находится в векторе.

P.P.S. Меня попросили отправить сборку, сгенерированную для функций поиска. Вот он:

found=find1(vec, vec.capacity());
001811D0  lea         eax,[esp+5Ch] 
001811D4  call        std::vector<unsigned int,std::allocator<unsigned int> >::capacity (1814D0h) 
001811D9  mov         esi,dword ptr [esp+60h] 
001811DD  mov         ecx,dword ptr [esp+64h] 
001811E1  cmp         esi,ecx 
001811E3  je          wmain+180h (1811F0h) 
001811E5  cmp         dword ptr [esi],eax 
001811E7  je          wmain+180h (1811F0h) 
001811E9  add         esi,4 
001811EC  cmp         esi,ecx 
001811EE  jne         wmain+175h (1811E5h) 



found=find2(vec, vec.capacity());
001812AE  lea         eax,[esp+5Ch] 
001812B2  call        std::vector<unsigned int,std::allocator<unsigned int> >::capacity (1814D0h) 
001812B7  mov         ecx,dword ptr [esp+60h] 
001812BB  mov         edx,dword ptr [esp+64h] 
001812BF  cmp         ecx,edx 
001812C1  je          wmain+262h (1812D2h) 
001812C3  cmp         dword ptr [ecx],eax 
001812C5  je          wmain+34Fh (1813BFh) 
001812CB  add         ecx,4 
001812CE  cmp         ecx,edx 
001812D0  jne         wmain+253h (1812C3h) 

find2 использует ecx-register вместо esi. В чем разница между этими двумя регистрами? Может ли быть, что esi предполагает, что указатель правильно выровнен и, следовательно, приносит дополнительную производительность?

Прочитайте некоторую ссылку на сборку ecx - это просто счетчик, тогда как esi - источник памяти. Поэтому я думаю, что алгоритм STL знает, что Итератор случайного доступа правильно выровнен и поэтому использует указатели на память. Где в версии, отличной от STL, нет предположений о том, как выполняется выравнивание. Я прав?

Ответ 1

Алгоритм Visual С++ find использует непроверенные итераторы, в то время как в вашей ручной петле используются проверенные итераторы.

<ы > Моя другая догадка заключается в том, что вы вызываете std::vector<t>::end() на каждой итерации вашего цикла в find2, а std::find приводит только к одному вызову для начального и конечного доступа. Я идиот.

Ответ 2

Убедитесь, что вы скомпилировали свой код в режиме выпуска с проверены итераторы отключены

Задайте _SECURE_SCL = 0 в определениях препроцессора.

Кроме того, boost:: progress_timer имеет разрешение миллисекунд, которое я считаю (оно основано на std:: clock), что делает его очень ненадежным для точных измерений коротких периодов времени. Вам необходимо сделать код, который вы измеряете значительно медленнее, чтобы избавиться от других факторов (например, ваш процесс находится в режиме ожидания и т.д.). Вы должны измерять с помощью высокопроизводительных счетчиков, как было предложено DeadMG.

Ответ 3

find не принимает value_type, он принимает значение const_type &. Теперь я бы сказал, что для unsigned int это не имеет значения. Однако вполне возможно, что ваш оптимизатор просто этого не заметил и не смог правильно оптимизировать тело цикла.

Изменить: я бы предположил, что вы относитесь к компилятору с использованием цикла for. Вы можете переписать его как

typename Vec::iterator i, end;
i = vec.begin();
end = vec.end();
while(i != end && *i != val)
    i++;
return i;

Конечно, парень, который написал std:: find, точно знает, насколько оптимизирован оптимизатор, и с чем именно он может и не может справиться.

Изменить: Я проверил ваш тест на своей машине. Это i7 930, без разгона, на Visual Studio 2010. Я заменил boost:: progress_timer счетчиком высокой производительности.

__int64 frequency, begin, end;
QueryPerformanceCounter(frequency);
double d;
QueryPerformanceCounter(begin);
iter found=find1(vec, vec.capacity());
QueryPerformanceCounter(end);
d = ((end - begin) / (double)frequency) * 1000000;
cout << "first search required: " << d << endl;
cout << "first search found value: " << value_found(vec, found)<< endl;


QueryPerformanceCounter(begin);
found=find2(vec, vec.capacity());
QueryPerformanceCounter(end);
d = ((end - begin) / (double)frequency) * 1000000;
cout << "second search required: " << d << endl;
cout << "second search found value: " << value_found(vec, found)<< endl;

Говорит, что для обоих из них требуется 0,24 (примерно) наносекунды для работы, т.е. нет никакой разницы. Мое предложение состоит в том, что ваш оптимизатор просто незрелый и ваша версия std:: find написана точно для того, чтобы представить правильную оптимизацию, тогда как ваша находка просто не отметит правильные поля оптимизации.

Изменить: ваши данные синхронизации явно выведены из строя. Мой i7 работает в 0.23 наносекундах, то есть 0,00000023 секунды, тогда как ваш хочет 0,008 секунды. Если мой i7 не будет примерно в 40 000 раз быстрее, чем ваш, нет никакого способа. Нет никакого способа, чтобы i7 занял так много времени, чтобы пройти только десять миллионов предметов. Конечно, я на самом деле запускаю 64-битную Windows 7, хотя и не скомпилировал ее в 64-битном режиме.

Теперь вы отправите дизассемблер.

FIND1:

00F810D3  mov         esi,dword ptr [esp+34h]  
00F810D7  mov         eax,dword ptr [esp+3Ch]  
00F810DB  mov         ecx,dword ptr [esp+38h]  
00F810DF  sub         eax,esi  
00F810E1  sar         eax,2  
00F810E4  cmp         esi,ecx  
00F810E6  je          main+0B3h (0F810F3h)  
00F810E8  cmp         dword ptr [esi],eax  
00F810EA  je          main+0B3h (0F810F3h)  
00F810EC  add         esi,4  
00F810EF  cmp         esi,ecx  
00F810F1  jne         main+0A8h (0F810E8h)  

find2:

00F8119A  mov         ecx,dword ptr [esp+34h]  
00F8119E  mov         eax,dword ptr [esp+3Ch]  
00F811A2  mov         edx,dword ptr [esp+38h]  
00F811A6  sub         eax,ecx  
00F811A8  sar         eax,2  
00F811AB  cmp         ecx,edx  
00F811AD  jae         main+17Fh (0F811BFh)  
00F811AF  nop  
00F811B0  cmp         dword ptr [ecx],eax  
00F811B2  je          main+254h (0F81294h)  
00F811B8  add         ecx,4  
00F811BB  cmp         ecx,edx  
00F811BD  jb          main+170h (0F811B0h)  
00F811BF  mov         esi,edx  

Вы можете видеть, что find2 немного отличается от find1. Я проверил, заменив вызов на find2 на другой вызов find1, который производит идентичную разборку. Любопытно, что они производят разные сборки.

Ответ 4

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

Некоторые вещи для проверки (вы можете рассмотреть некоторые из этих очевидных):

  • Какие параметры оптимизации вы используете? Вы тестируете сборку релизов, правильно?
  • Вы сказали, что вы проверили код сборки, сгенерированный версией STL, и не используете векторию. Но может быть, он использует какой-то другой общий метод оптимизации, такой как разворот цикла?
  • Почему вы используете i < end, а не i != end в своем цикле? (Я действительно сомневаюсь, что это имеет значение, но кто знает?)

(Мой оригинальный ответ был абсолютно глупым - не уверен, почему он получил голосование - я оставляю его здесь, так как некоторые комментарии касаются его)

В этом случае я подозреваю, что вы просто видите эффекты иерархии памяти. Когда вы вызываете find1(), CPU должен считывать все данные из ОЗУ. Эти данные затем будут храниться в кэше ЦП, что значительно (легко в 10-100 раз) быстрее, чем доступ к ОЗУ. Когда вы вызываете find2(), CPU может считывать весь массив из кэш-памяти, и поэтому find2() занимает меньше времени для выполнения.

Чтобы получить еще несколько доказательств, попробуйте поменять код так, чтобы сначала вы измерили find2(), а затем find1(). Если ваши результаты будут отменены, вероятно, вы увидите эффект от кеша. Если они этого не сделают, то это что-то еще.

Edit После некоторой мысли (на самом деле, сразу после некоторой мысли), я думаю, что мое первоначальное подозрение должно быть неправильным (размер массива, который вы просматриваете, делает маловероятным, чтобы весь массив был помещен в кеш). По-прежнему могут быть эффекты кеша, но они, вероятно, несколько более тонкие. Тем не менее, попробуйте изменить измерения в любом случае, было бы интересно посмотреть, какой эффект он имеет.

Ответ 5

Я сам не использую Visual С++, но с GCC я также получил результат, который find2 немного медленнее. Тем не менее, я смог сделать find2 чуть быстрее, чем find1, вручную развернув цикл:

template<class Vec>
typename Vec::const_iterator find2(Vec const& v, typename Vec::value_type val)
{
  for(typename Vec::const_iterator i=v.begin(), end=v.end(); i != end; ) {
    if (i[0]==val) return i;
    if (i[1]==val) return i + 1;
    i += 2;
  }
  return v.end();
}

Мое предположение, почему std::find быстрее, заключается в том, что компилятор имеет всю информацию, чтобы выяснить, что размер вектора кратен 2, и что это можно сделать для разворачивания.

Другое предположение, что это просто компромисс между пространством/размером - компилятор пропускает эту оптимизацию в общем случае.

Ответ 6

Многие пользователи C/С++ жалуются, что, как только они напишут специализацию функции, ее выполняет неспециализированная версия!

Причина заключается в том, что просто как только вы пишете пропуски оптимизации на своем конце компилятора, вы подумаете о способах улучшения генерации кода std::find, и, следовательно, он выполняет вашу реализацию.

Также node, что std::find для VС++, по крайней мере, имеет разные версии, которые будут вызывать разные функции и алгоритмы поиска для разных типов итераторов.

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