LRU 缓存和多线程

LRU Caching & Multithreading

本文关键字:多线程 缓存 LRU      更新时间:2023-10-16

我之前已经发了一篇帖子,询问LRU缓存的良好设计(在c++中)。你可以在这里找到问题、答案和一些代码:

更好地理解LRU算法

我现在已经尝试多线程这段代码(使用pthread),并得到了一些真正意想不到的结果。在尝试使用锁之前,我已经创建了一个系统,其中每个线程访问自己的缓存(参见代码)。我在4核处理器上运行这段代码。我试着用1个线程和4个线程运行它。当它在1个线程上运行时,我在缓存中进行100万次查找,在4个线程上,每个线程进行250K次查找。我期望得到一个时间减少与4线程,但得到相反的。1个线程运行2.2秒,4个线程运行超过6秒??我就是搞不懂这个结果。

我的代码有问题吗?这可以解释一下吗(线程管理需要时间)。如果能得到专家的反馈就太好了。谢谢-

我用以下命令编译这段代码:c++ -o 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 %dn", (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.%ldn", 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()正在访问unordered_mapsshared_ptrs实现的CacheLRU。特别是unordered_maps,可能会在下面使用某种全局互斥锁来实现,这会导致所有线程竞争访问权。

要真正诊断这里发生了什么,你应该在testCache()中做一些更简单的事情。(首先尝试将输入变量的平方根()取250K次(vs. 1M次)。然后尝试线性访问大小为250K(或1M)的C数组。慢慢积累你正在做的复杂的事情。)

另一种可能与pthread_join有关。pthread_join直到所有线程完成才返回。所以如果一个比其他的花的时间长,你测量的是最慢的一个。您的计算似乎是平衡的,但也许您的操作系统做了一些意想不到的事情?(比如将多个线程映射到一个核心(可能是因为您有一个超线程处理器?,或者一个线程在运行过程中从一个内核移动到另一个内核(可能是因为操作系统认为它是聪明的,当它不是。)

这将是一个有点"建立它"的答案。我在Fedora 16 Linux系统上运行您的代码,该系统具有4核AMD cpu和16GB RAM。

我可以确认我看到了类似的"线程越多越慢"的行为。我去掉了随机函数,这一点也没有改善。