Я уже сделал пост некоторое время назад, чтобы спросить о хорошем дизайне для кэширования LRU (в C ++). Вы можете найти вопрос, ответ и код там:
Лучшее понимание алгоритма LRU
Сейчас я попытался использовать многопоточность этого кода (используя pthread) и получил некоторые действительно неожиданные результаты. Прежде чем пытаться использовать блокировку, я создал систему, в которой каждый поток обращается к своему кешу (см. Код). Я запускаю этот код на 4-ядерном процессоре. Я пытался запустить его с 1 потока и 4 потока. Когда он работает в 1 потоке, я делаю 1 миллион просмотров в кеше, в 4 потоках каждый поток выполняет 250K просмотров. Я ожидал получить сокращение времени с 4 потоками, но получилось наоборот. 1 поток выполняется за 2,2 секунды, 4 потока — более чем за 6 секунд. Я просто не могу понять этот результат.
Что-то не так с моим кодом? Можно ли это как-то объяснить (управление потоками требует времени). Было бы здорово получить отзывы экспертов. Большое спасибо —
Я компилирую этот код с помощью: c ++ -o cache cache.cpp -std = c ++ 0x -O3 -lpthread
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>
#include <sys/time.h>
#include <list>
#include <cstdlib>
#include <cstdio>
#include <memory>
#include <list>
#include <unordered_map>
#include <stdint.h>
#include <iostream>
typedef uint32_t data_key_t;
using namespace std;
//using namespace std::tr1;
class TileData
{
public:
data_key_t theKey;
float *data;
static const uint32_t tileSize = 32;
static const uint32_t tileDataBlockSize;
TileData(const data_key_t &key) : theKey(key), data(NULL)
{
float *data = new float [tileSize * tileSize * tileSize];
}
~TileData()
{
/* std::cerr << "delete " << theKey << std::endl; */
if (data) delete [] data;
}
};
typedef shared_ptr<TileData> TileDataPtr; // automatic memory management!
TileDataPtr loadDataFromDisk(const data_key_t &theKey)
{
return shared_ptr<TileData>(new TileData(theKey));
}
class CacheLRU
{
public:
list<TileDataPtr> linkedList;
unordered_map<data_key_t, TileDataPtr> hashMap;
CacheLRU() : cacheHit(0), cacheMiss(0) {}
TileDataPtr getData(data_key_t theKey)
{
unordered_map<data_key_t, TileDataPtr>::const_iterator iter = hashMap.find(theKey);
if (iter != hashMap.end()) {
TileDataPtr ret = iter->second;
linkedList.remove(ret);
linkedList.push_front(ret);
++cacheHit;
return ret;
}
else {
++cacheMiss;
TileDataPtr ret = loadDataFromDisk(theKey);
linkedList.push_front(ret);
hashMap.insert(make_pair<data_key_t, TileDataPtr>(theKey, ret));
if (linkedList.size() > MAX_LRU_CACHE_SIZE) {
const TileDataPtr dropMe = linkedList.back();
hashMap.erase(dropMe->theKey);
linkedList.remove(dropMe);
}
return ret;
}
}
static const uint32_t MAX_LRU_CACHE_SIZE = 100;
uint32_t cacheMiss, cacheHit;
};
int numThreads = 1;
void *testCache(void *data)
{
struct timeval tv1, tv2;
// Measuring time before starting the threads...
double t = clock();
printf("Starting thread, lookups %d\n", (int)(1000000.f / numThreads));
CacheLRU *cache = new CacheLRU;
for (uint32_t i = 0; i < (int)(1000000.f / numThreads); ++i) {
int key = random() % 300;
TileDataPtr tileDataPtr = cache->getData(key);
}
std::cerr << "Time (sec): " << (clock() - t) / CLOCKS_PER_SEC << std::endl;
delete cache;
}
int main()
{
int i;
pthread_t thr[numThreads];
struct timeval tv1, tv2;
// Measuring time before starting the threads...
gettimeofday(&tv1, NULL);
#if 0
CacheLRU *c1 = new CacheLRU;
(*testCache)(c1);
#else
for (int i = 0; i < numThreads; ++i) {
pthread_create(&thr[i], NULL, testCache, (void*)NULL);
//pthread_detach(thr[i]);
}
for (int i = 0; i < numThreads; ++i) {
pthread_join(thr[i], NULL);
//pthread_detach(thr[i]);
}
#endif
// Measuring time after threads finished...
gettimeofday(&tv2, NULL);
if (tv1.tv_usec > tv2.tv_usec)
{
tv2.tv_sec--;
tv2.tv_usec += 1000000;
}
printf("Result - %ld.%ld\n", tv2.tv_sec - tv1.tv_sec,
tv2.tv_usec - tv1.tv_usec);
return 0;
}
Тысяча извинений, продолжая отлаживать код, я понял, что совершил очень плохую ошибку новичка, если вы посмотрите на этот код:
TileData(const data_key_t &key) : theKey(key), data(NULL)
{
float *data = new float [tileSize * tileSize * tileSize];
}
из класса TikeData, где данные, как предполагается, фактически являются переменными-членами класса … Таким образом, правильный код должен быть:
class TileData
{
public:
float *data;
TileData(const data_key_t &key) : theKey(key), data(NULL)
{
data = new float [tileSize * tileSize * tileSize];
numAlloc++;
}
};
Я так сожалею об этом! Это ошибка, которую я сделал в прошлом, и я думаю, что создание прототипов — это здорово, но иногда это приводит к таким глупым ошибкам.
Я запустил код с 1 и 4 потоками и теперь вижу ускорение. 1 поток занимает около 2,3 секунд, 4 потока занимает 0,92 секунды.
Спасибо всем за вашу помощь, и извините, если я заставил вас терять время 😉
У меня пока нет конкретного ответа. Я могу думать о нескольких возможностях. Во-первых testCache()
использует random()
, который почти наверняка реализован с одним глобальным мьютексом. (Таким образом, все ваши потоки конкурируют за мьютекс, который теперь пинг-понг между кешами.) ((Это предполагает, что random()
на самом деле потокобезопасен в вашей системе.))
Следующий, testCach()
получает доступ к CacheLRU
который реализован с unordered_maps
а также shared_ptrs
, unordered_maps
в частности, может быть реализован с каким-то глобальным мьютексом, который заставляет все ваши потоки конкурировать за доступ.
Чтобы действительно диагностировать то, что здесь происходит, вы должны сделать что-то гораздо более простое внутри testCache()
, (Сначала попробуйте просто взять sqrt () входной переменной 250K раз (по сравнению с 1M раз). Затем попробуйте получить линейный доступ к массиву C размером 250K (или 1M). Медленно соберите сложную вещь, которую вы сейчас делаете.)
Другая возможность связана с pthread_join
, pthread_join
не возвращается, пока не завершены все темы. Поэтому, если один занимает больше времени, чем другие, вы измеряете самый медленный. Ваши вычисления здесь кажутся сбалансированными, но, возможно, ваша ОС делает что-то неожиданное? (Например, отображение нескольких потоков на одно ядро (возможно, из-за того, что у вас гиперпоточный процессор?, Или один поток перемещается из одного ядра в другое в середине цикла) (возможно, потому, что ОС думает, что это умно, когда это не так. )
Это будет что-то вроде ответа «построить его». Я использую ваш код в системе Fedora 16 Linux с 4-ядерным процессором AMD и 16 ГБ оперативной памяти.
Я могу подтвердить, что вижу подобное поведение «медленнее с большим количеством потоков». Я удалил случайную функцию, которая ничего не улучшает.
Я собираюсь внести некоторые другие незначительные изменения.