2013-11-14 5 views
6

Я играл на днях, пытаясь понять, как далеко я могу что-то оптимизировать. Я решил начать с простой карты, которая просто выполняет линейный поиск, чтобы найти, есть ли элемент, а затем попытаться оптимизировать большую часть его. Кроме того, для сравнения, я делаю то же самое со std :: map и std :: vector, используя std :: find.Почему std :: vector так быстро (или моя реализация слишком медленная)

Результаты с картой - ожидаемые, более медленное создание и разрушение, чем моя карта, но гораздо более высокая скорость (на самом деле, я не смог ее измерить, он всегда возвращает 0). Проблема с std :: vector. Я ожидал, что это будет медленнее, чем моя реализация, но это не так, и я действительно не понимаю, как это может быть таким же или быстрее, поскольку моя реализация пропускает худший случай (значение не находится в векторе) и используя кеш результатов.

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

Результаты тестирования (i3, Windows 8.1 Pro 64, Visual Studio 2013):

std::vector : 
    Build : 85.0042 ms 
    Loop : 37.0011 ms 
    Find : 1.82259 ms -> First : Found, Second : Found, Third : Not Found 
    Release : 0 ms 
-------------------- 
std::map : 
    Build : 6929.41 ms 
    Loop : 570.032 ms 
    Find : 0 ms -> First : Found, Second : Found, Third : Not Found 
    Release : 1425.08 
-------------------- 
Linear Map V0: 
    Build : 194.012 ms 
    Loop : 49.0052 ms 
    Find : 1.88915 ms -> First : Found, Second : Found, Third : Not Found 
    Release : 109.004 

Вот код карты:

template<typename T> 
class LinearMap0 
{ 
public: 
LinearMap0() 
{ 
    _end = _root = new Node; 
    _prebuffer = nullptr; 
    prebufferCapacity = 0; 
    _alive = true; 
    prebufferMarker = 0; 
    _cache = _mm_set1_epi32(-1); 
    for (auto& ptr : _cacheBuffer) ptr = nullptr; 
    MinID = INT32_MAX - 1; 
    MaxID = -1; 
} 
void PreAllocate(int Count) 
{ 
    prebufferCapacity = Count; 
    _prebuffer = new Node[Count]; 
} 
~LinearMap0() 
{ 
    if (_alive) 
    { 
     Release(); 
    } 
} 
void Release() 
{ 
    Node* marker = _end; 
    while (marker->Prev) 
    { 
     marker = marker->Prev; 
     if (!marker->Next->IsPreAllocated) delete marker->Next; 
    } 

    if (!_root->IsPreAllocated) delete _root; 
    delete[] _prebuffer; 

    _alive = false; 
} 

void AddElement(int ID,T element) 
{ 
    Node* tmp = nullptr; 
    if (prebufferMarker < prebufferCapacity) 
    { 
     // Use a pre-allocated object 
     tmp = &_prebuffer[prebufferMarker]; 
     prebufferMarker++; 
     tmp->IsPreAllocated = true; 
    } 
    else 
    { 
     tmp = new Node; 
    } 

    tmp->ID = ID; 
    tmp->Data = element; 

    // Update list 
    _end->Next = tmp; 
    Node* prevEnd = _end; 
    _end = tmp; 
    _end->Prev = prevEnd; 
    bool isMin = ID < MinID; MinID = ID * isMin + (1 - isMin) * MinID; 
    bool isMax = ID > MaxID; MaxID = ID * isMax + (1 - isMax) * MaxID; 
} 
void DeleteLast() 
{ 
    Node* tmp = _end; 

    _end = _end->Prev; 
    _end->Next = nullptr; 

    delete tmp; 
} 

template<class Function> 
void Loop(Function&& f, bool Forward = true) 
{ 
    if (Forward) 
    { 
     Node* marker = _root; 
     while (marker->Next) 
     { 
      marker = marker->Next; 
      f(marker->Data); 
     } 
    } 
    else 
    { 
     Node* marker = _end; 
     while (marker->Prev) 
     { 
      marker = marker->Prev; 
      f(marker->Data); 
     } 
    } 
} 

T* Find(int ID) 
{ 
    // Bounds check 
    if (ID < MinID || ID > MaxID) return nullptr; 

    // Check it it's in the cache 

    // Compare the value to every value in the cache 
    __m128i idxSSE = _mm_set1_epi32(ID); 
    __m128i C = _mm_cmpeq_epi32(_cache, idxSSE); 

    // To change form -1 to 1 
    C = _mm_mul_epi32(C, _mm_set1_epi32(-1)); 

    // Now C holds 1 if true, or 0 if false (in each of its 4 members). It should only be ONE set at 1 
    __m128i tmp = _mm_set1_epi32(1); 
    __m128i S = _mm_sub_epi32(tmp, C); 

    // Now find the index 
    int i = S.m128i_i32[0] * (C.m128i_i32[1] + S.m128i_i32[1] * (2 * C.m128i_i32[2] + S.m128i_i32[2] * (3 * C.m128i_i32[3] + S.m128i_i32[3] * -1))); 

    if (i != -1) 
     return _cacheBuffer[i]; 

    // Traverse the list 
    Node* marker0 = _root; 
    T* obj = nullptr; 

    while (true) 
    { 
     if (marker0->ID == ID) 
     { 
      obj = &marker0->Data; 
     } 

     if (marker0->Next) marker0 = marker0->Next; else break; 
    } 

    // Cache value and return 
    _cache.m128i_i32[cacheMarker] = ID; 
    _cacheBuffer[cacheMarker] = obj; 
    cacheMarker = (cacheMarker + 1) & 3; // x & 3 = x % 4 

    return obj; 
} 
private: 
struct Node 
{ 
    Node() 
    { 
     Prev = nullptr; 
     Next = nullptr; 
     IsPreAllocated = false; 
     ID = -1; 
    } 
    T Data; 
    Node* Prev; 
    Node* Next; 
    bool IsPreAllocated; 
    int ID; 
}; 

Node* _root; 
Node* _end; 

Node* _prebuffer; 
int prebufferCapacity; 
int prebufferMarker; 

bool _alive; 

__m128i _cache; 
T* _cacheBuffer[4]; 
int cacheMarker; 
int MinID, MaxID; 
}; 

А вот тест:

// Initialize seeds 
const __int64 ecount = 5 * 1000*1000; 
vector<__int64> seed(ecount); 
for (__int64 i = 0; i < ecount; i++) 
{ 
    seed[i] = i; 
} 
random_shuffle(seed.begin(), seed.end()); 

///////////// std::vector 

vector<__int64> v; 

cout << "--------------------" << endl; 
cout << "std::vector :" << endl; 
cout << " Build : " << time_call([&]() 
{ 
    v.resize(ecount/2); 
    for (__int64 i = 0; i < ecount; i++) 
    { 
     if (i < (ecount/2)) 
      v[i] = seed[i]; 
     else 
      v.push_back(seed[i]); 
    } 
}) << " ms" << endl; 

cout << " Loop : " << time_call([&]() 
{ 
    for (auto& n : v) 
     n /= 2; 
}) << " ms" << endl; 

bool found1, found2, found3; 
cout << " Find : " << (((float)time_call([&]() 
{ 
    for (int i = 0; i < 15; i++) 
    { 
     // Should exist 
     found1 = find(v.begin(), v.end(), seed[5]/2) != v.end();//find(seed[5]) != m.end(); 
     found2 = find(v.begin(), v.end(), seed[1000]/2) != v.end(); 

     // Shouldn't exist 
     found3 = find(v.begin(), v.end(), -1234) != v.end(); 
    } 
}))/15.0)/3.0; 
cout << " ms " << " -> First : " << ((found1) ? "Found" : "Not Found") << ", Second : " << ((found2) ? "Found" : "Not Found") << ", Third : " << ((found3) ? "Found" : "Not Found") << endl; 

cout << " Release : " << time_call([&]() 
{ 
    v.clear(); 
}) << " ms" << endl; 

///////////// std::map 

map<__int64, __int64> m; 

cout << "--------------------" << endl; 
cout << "std::map :" << endl; 
cout << " Build : " << time_call([&]() 
{ 
    for (__int64 i = 0; i < ecount; i++) 
    { 
     m[seed[i]] = seed[i]; 
    } 
}) << " ms" << endl; 

cout << " Loop : " << time_call([&]() 
{ 
    for (auto& n : m) 
     n.second /= 2; 
}) << " ms" << endl; 

cout << " Find : " << (((float)time_call([&]() 
{ 
    for (int i = 0; i < 15; i++) 
    { 
     // Should exist 
     found1 = m.find(seed[5]) != m.end(); 
     found2 = m.find(seed[1000]) != m.end(); 

     // Shouldn't exist 
     found3 = m.find(-1234) != m.end(); 
    } 
}))/15.0)/3.0; 
cout << " ms " << " -> First : " << ((found1) ? "Found" : "Not Found") << ", Second : " << ((found2) ? "Found" : "Not Found") << ", Third : " << ((found3) ? "Found" : "Not Found") << endl; 

cout << " Release : " << time_call([&]() 
{ 
    m.clear(); 
}) << endl; 

///////////// Linear Map V0 

LinearMap0<__int64> c; 

cout << "--------------------" << endl; 
cout << "Linear Map V0:" << endl; 
cout << " Build : " << time_call([&]() 
{ 
    c.PreAllocate(ecount/2); 
    for (__int64 i = 0; i < ecount; i++) 
    { 
     c.AddElement(seed[i],seed[i]); 
    } 
}) << " ms" << endl; 

cout << " Loop : " << time_call([&]() 
{ 
    c.Loop([](__int64& Data) 
    { 
     Data /= 2; 
    }); 
}) << " ms" << endl; 

cout << " Find : " << (((float)time_call([&]() 
{ 
    for (int i = 0; i < 15; i++) 
    { 
     // Should exist 
     found1 = c.Find(seed[5]); 
     found2 = c.Find(seed[1000]); 

     // Shouldn't exist 
     found3 = c.Find(-1234); 
    } 
}))/15.0)/3.0; 
cout << " ms -> First : " << ((found1) ? "Found" : "Not Found") << ", Second : " << ((found2) ? "Found" : "Not Found") << ", Third : " << ((found3) ? "Found" : "Not Found") << endl; 

cout << " Release : " << time_call([&]() 
{ 
    c.Release(); 
}) << endl; 

EDIT: time_call is:

template <class Function> 
double time_call(Function&& f) 
{ 
    chrono::time_point<chrono::high_resolution_clock> start, end; 
    start = chrono::high_resolution_clock::now(); 
     f(); 
    end = chrono::high_resolution_clock::now(); 

    return ((double)(chrono::duration_cast<chrono::nanoseconds>(end - start).count()))/1000000.0; 
} 
+5

Вы внесли связанный список.'std :: vector' - это массив с динамическим размером. – Adam

+5

Почему вы используете не-std-файлы, такие как '__int64' вместо' std :: int64_t'? – Walter

+0

Я знаю, но вся разница в производительности - это просто кеш процессора? Поскольку связанный список и массивы с динамическим размером не такие разные, и, как я уже сказал, у меня всего 2 находок, а все остальные 43 выходят раньше, будь то проверка кешей или границ. –

ответ

11

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

Подход связанного списка имеет такие преимущества, как возможность вставки элементов без каких-либо перераспределений.

Однако подход массива имеет ряд существенных преимуществ:

  • линейный поиск просто сканирует память, которая является именно то, что кэши и предварительно fetchers построены для. Сканирование связанного списка будет менее эффективным, потому что каждый переход в нераскрытую память означает дорогостоящий промах кеша.
  • Сканирование линейного массива легко прорисует. Если вы скомпилируете с -O3, то, скорее всего, компилятор будет использовать векторизованную версию std::find. Из-за зависимостей памяти невозможно провести векторную привязку проверки связанного списка.
  • количество памяти б/у. Ваш связанный список должен содержать указатель next, который эффективно делает ваши элементы более крупными. Кроме того, каждый непереадресованный Node должен оплачивать служебные данные распределителя (то есть данные учета для new и delete). Это означает, что вы нажмете ограничения пропускной способности памяти раньше, и вы можете поместить меньше элементов в кеш.
+0

Ну, теперь, когда вы это говорите, это имеет смысл. Думаю, я сделал очень быстрый список. –

+0

PS: Не так быстро на самом деле: станд :: Список: Телосложение: 292.019 мс Loop: 49.0047 мс поиск: 9.73399 мс -> Первый: Найдено, второе: Найдено Третье: Не найдено выпуска: 150.009 мс –

+5

Не бить себя. Мы все пытаемся победить STL, но смиряем его годы и годы оптимизации людьми, которые знают, что они делают. Лучше всего только изобретать новый тип контейнера с меньшей сложностью (т. Е. Более быстрый большой-ой) или использовать ярлыки, которые работают только над вашей проблемой (т. Е. Ломаются в общем случае, поэтому STL не может их использовать). – Adam

1

Все преимущества вектора std состоят в том, что элементы плотно упакованы (элемент 1 в памяти сразу после элемента 0 и т. Д.). Это большое преимущество для CPU, потому что чтение в памяти намного более предсказуемо. Когда у вас есть узлы, выделенные в куче, процессор должен прыгать назад и вперед, как сумасшедший, чтобы извлечь память.

Отъезд this нить.

Смежные вопросы