【问题标题】:How can I make my simple .NET LRU cache faster?如何让我的简单 .NET LRU 缓存更快?
【发布时间】:2010-10-11 13:36:53
【问题描述】:

昨晚和今晚,我尝试了几种不同的方法,并提出了一种类似于下面 Jeff 提出的方法(我什至已经完成了他在更新中提出的建议,并将我自己的简单 LL 实现放在一起收益)。这是代码,在这一点上它看起来不再特别干净,但我已经经历了无数次改变我所能做的任何事情来提高性能。

public class NewLRU2<K, V> where V : class
{
    int m_iMaxItems;
    Dictionary<K, LRUNode<K, V>> m_oMainDict;

    private LRUNode<K,V> m_oHead;
    private LRUNode<K,V> m_oTail;
    private LRUNode<K,V> m_oCurrent;

    public NewLRU2(int iSize)
    {
        m_iMaxItems = iSize;
        m_oMainDict = new Dictionary<K, LRUNode<K,V>>();

        m_oHead = null;
        m_oTail = null;
    }

    public V this[K key]
    {
        get
        {
            m_oCurrent = m_oMainDict[key];

            if (m_oCurrent == m_oHead)
            {
                //do nothing
            }
            else if (m_oCurrent == m_oTail)
            {
                m_oTail = m_oCurrent.Next;
                m_oTail.Prev = null;

                m_oHead.Next = m_oCurrent;
                m_oCurrent.Prev = m_oHead;
                m_oCurrent.Next = null;
                m_oHead = m_oCurrent;
            }
            else
            {
                m_oCurrent.Prev.Next = m_oCurrent.Next;
                m_oCurrent.Next.Prev = m_oCurrent.Prev;

                m_oHead.Next = m_oCurrent;
                m_oCurrent.Prev = m_oHead;
                m_oCurrent.Next = null;
                m_oHead = m_oCurrent;
            }
            
            return m_oCurrent.Value;
        }
    }

    public void Add(K key, V value)
    {
        if (m_oMainDict.Count >= m_iMaxItems)
        {   
            //remove old
            m_oMainDict.Remove(m_oTail.Key);

            //reuse old
            LRUNode<K, V> oNewNode = m_oTail;
            oNewNode.Key = key;
            oNewNode.Value = value;
            
            m_oTail = m_oTail.Next;
            m_oTail.Prev = null;

            //add new
            m_oHead.Next = oNewNode;
            oNewNode.Prev = m_oHead;
            oNewNode.Next = null;
            m_oHead = oNewNode;
            m_oMainDict.Add(key, oNewNode);
        }
        else
        {
            LRUNode<K, V> oNewNode = new LRUNode<K, V>(key, value);
            if (m_oHead == null)
            {
                m_oHead = oNewNode;
                m_oTail = oNewNode;
            }
            else
            {
                m_oHead.Next = oNewNode;
                oNewNode.Prev = m_oHead;
                m_oHead = oNewNode;
            }
            m_oMainDict.Add(key, oNewNode);
        }
    }

    public bool Contains(K key)
    {
        return m_oMainDict.ContainsKey(key);
    }
}


internal class LRUNode<K,V>
{
    public LRUNode(K key, V val)
    {
        Key = key;
        Value = val;
    }

    public K Key;
    public V Value;
    public LRUNode<K, V> Next;
    public LRUNode<K, V> Prev;
}

有一些看起来/感觉很奇怪的部分——比如在进行添加时重用旧节点——但我能够从它们中获得可观的性能提升。我也对从节点上的实际属性切换到公共变量所产生的差异感到有些惊讶,但我想这就是它与这些东西的关系。在这一点上,上面的代码几乎完全受到字典操作的性能限制,所以我不确定我是否会从混搭它中得到更多。我会继续思考并查看一些回复。

来自原始帖子的解释: 大家好。 因此,我编写了一个简单的轻量级 LRU 实现以用于压缩库(我使用它在基于散列的 LZW 样式的输入中查找匹配的字节字符串),并且我正在寻找制作方法它更快。

【问题讨论】:

标签: .net performance data-structures caching lru


【解决方案1】:

更新 #2

这减少了对链表 Remove 进行列表遍历的需要。它引入了一个同时具有键和值的 LruCacheNode。该密钥仅在您修剪缓存时使用。如果您编写自己的链表实现,其中每个节点本质上是一个 LruCacheNode 以及一个 Next 和 Back 引用,您可以获得更好的性能。这就是 LinkedHashMap 所做的事情(请参阅thesetwo 问题)。

public class LruCache<K, V>
{
    private readonly int m_iMaxItems;
    private readonly Dictionary<K, LinkedListNode<LruCacheNode<K, V>>> m_oMainDict;
    private readonly LinkedList<LruCacheNode<K, V>> m_oMainList;

    public LruCache(int iSize)
    {
        m_iMaxItems = iSize;
        m_oMainDict = new Dictionary<K, LinkedListNode<LruCacheNode<K, V>>>();
        m_oMainList = new LinkedList<LruCacheNode<K, V>>();
    }

    public V this[K key]
    {
        get
        {
            return BumpToFront(key).Value;
        }
        set
        {
            BumpToFront(key).Value = value;
        }
    }

    public void Add(K key, V value)
    {
        LinkedListNode<LruCacheNode<K, V>> newNode = m_oMainList.AddFirst(new LruCacheNode<K, V>(key, value));
        m_oMainDict.Add(key, newNode);

        if (m_oMainList.Count > m_iMaxItems)
        {
            m_oMainDict.Remove(m_oMainList.Last.Value.Key);
            m_oMainList.RemoveLast();
        }
    }

    private LruCacheNode<K, V> BumpToFront(K key)
    {
        LinkedListNode<LruCacheNode<K, V>> node = m_oMainDict[key];
        if (m_oMainList.First != node)
        {
            m_oMainList.Remove(node);
            m_oMainList.AddFirst(node);
        }
        return node.Value;
    }

    public bool Contains(K key)
    {
        return m_oMainDict.ContainsKey(key);
    }
}

internal sealed class LruCacheNode<K, V>
{
    private readonly K m_Key;
    private V m_Value;

    public LruCacheNode(K key, V value)
    {
        m_Key = key;
        m_Value = value;
    }

    public K Key
    {
        get { return m_Key; }
    }

    public V Value
    {
        get { return m_Value; }
        set { m_Value = value; }
    }
}

您必须对事物进行概要分析,看看这是否对您的环境有所改善。

次要更新:我更新了 BumpToFront,以根据 Tim Stewart 的评论检查节点是否已经位于最前面。

【讨论】:

  • 我试过这段代码,不幸的是它破坏了性能。现在,所有其他操作都被 Contains 相形见绌,它现在使用 96% 的执行时间——我预计由于每次查找都会遍历整个列表。
  • 再试一次,我更新了它以使用 HashSet 来优化 .Contains 代码。如果你不能使用 HashSet 因为你在 3.5 之前工作,你可以用 Dictionary 替换它
  • 非常好。 @Jeff 我可以建议您使用 Dict 来享受更好的 JIT 吗?该建议已传递给我,因此您可以利用可能已经 JIT 的 Dict 而不是 Dict
  • 实际上,在进行了一些运行时分析之后,看起来你的速度较慢 D:当然看起来更好。
  • 你测试了什么类型的访问模式?
【解决方案2】:

LRU 缓存的目的不是让您修剪缓存并丢弃最近最少使用的东西吗? :-) 我没有看到任何修剪缓存的代码。既然您很可能想要检索用例的高性能,而修剪用例不太重要,为什么不将列表维护转移到修剪过程呢?

IOW,只需将条目放入缓存中,但在检索时为它们加上时间戳。不要重新排序条目,只需在使用时标记它们。可以是真正的 DateTime 时间戳,也可以是类中的简单计数器,最近使用的最高数字。然后在修剪过程中,只需遍历整个树并删除带有最旧图章的条目。

【讨论】:

    【解决方案3】:

    使用硬件缓存,而不是说 128 个元素,并保持项目 1-128 的顺序,您可以将其设置为 32 x 4,即 32 行,每行 4 个元素。地址的前 5 位将确定该地址将映射到 32 行中的哪一行,然后您将仅搜索 4 个项目,如果未找到则替换 4 个中最旧的。

    这要快得多,并且 IIRC 在 1 x 128 缓存的命中率的 10% 以内。

    要翻译,您将使用多个链表而不是一个链表,因此遍历它们要快得多。您必须有一种方法来确定特定项目映射到哪个列表。

    关键是,随着列表大小的增加,您尝试以完美的准确性保持列表中每个元素的确切顺序所获得的回报会递减。使用无序列表甚至可能会更好,并在缓存未命中时随机替换任何元素。取决于列表的大小,以及错过的惩罚与维护列表的成本。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-24
      • 1970-01-01
      • 2011-03-02
      • 1970-01-01
      相关资源
      最近更新 更多