News for hackers. Stuff that matters.
Re:Sieve is simpler than LRU by ricardobeat on Wednesday January 03, 2024 @07:49AM (#38853449)
> This is for no good reason other than just luck.

> When the Hand finds a victim we just replace the Hand slot with the new item, and mark it visited

That would cause newly inserted items to take at least N (size of the cache) rounds to be evicted. Evicting cache entries early is extremely important for performance. Their algorithm means new items will have a higher chance of being evicted, and that is by design, not random chance.

Here's the paper: https://yazhuozhang.com/assets/pdf/nsdi24-sieve.pdf

Re:Sieve is simpler than LRU by iainmerrick on Wednesday January 03, 2024 @12:39PM (#38856976)
I was curious too so I took a look. I think this is the relevant bit:

Recent work [94] shows that “lazy promotion” and “quick demotion” are two important properties of efficient cache eviction algorithms.

[...]

Quick demotion removes most objects quickly after they are inserted. Many previous works have discussed this idea in the context of evicting pages from a scan [16,60,67,70,75,77]. Recent work also shows that not only storage workloads but web cache workloads also benefit from quick demotion [94] because object popularity follows a power-law distribution, and many objects are unpopular.

[...]

[94] Juncheng Yang, Ziyue Qiu, Yazhuo Zhang, Yao Yue, and K. V. Rashmi. FIFO can be better than LRU: the power of lazy promotion and quick demotion. In The 19th Workshop on Hot Topics in Operating Systems (HotOS 23), 2023.

Re:Sieve is simpler than LRU by kazinator on Wednesday January 03, 2024 @01:07PM (#38857383)
I read that before.

Caching is predicated on locality. We put things into a cache based on the idea that something accessed once is likely to be used again, due to locality.

Lazy promotion is useful in a situation in which we don't trust that the access pattern exhibits locality, nor not fully. Perhaps it exhibits locality for some data, but is widely scattered for others.

For instance, an algorithm is working with numerous local variables in a loop (good locality) but the loop is also stepping through a very large set of data in memory, much larger than the cache. Moreover, items in this set are visited only once (poor/nonexistent locality).

If we regard accesses with a bit of suspicion ("this might not have good locality") we can avoid those latter items from overstaying in the cache. Items in the cache that are not going to be touched again effectively shrink the cache. Like if at any given time about half our cache has such items, the cache is effectively halved.

With a deterministic, fixed distance between the Hand pointer and the Insert point, which we can tune, we could empirically adjust for this. For instance we could have it so that, consistently, every new item that is not visited within 4 subsequent cache misses will be evicted.

If we had a magic oracle, we would not use LRU or any of these other approaches. We would ask the oracle questions "Great Oracle, given these items in the cache, and this new one which is not in the cache, which one(s) will not be seen again, or else which one(s) will be seen again farthest into the future?" That's really what we want to know, but since oracles don't exist, we use heuristics, like the idea that the longer an item goes without being accessed, the more likely it is that it's a never-again-accessed item.

Re:Sieve is simpler than LRU by 1a1a11a on Thursday January 04, 2024 @02:26PM (#38871158)
Great point! We tried the oracle (Belady MIN) and found that the oracle often does not admit new objects (<20% chance admitting new objects) in most traces. That's why we need to evict new objects quickly
Re:Sieve is simpler than LRU by kazinator on Wednesday January 03, 2024 @12:59PM (#38857269)
I clearly articulated in my comment that this is one choice we could make for the distance between the Insert point and the Hand pointer. That distance is a parameter whose space can be experimentally tested.

Without my proposal, there is no control over the parameter: the distance is an average, and so about half the cache size. When Hand is on the far right, a new item inserted at that time will stay in the cache long. When the Hand is far left, it may get evicted almost immediately.

The only thing important for performance is a good hit ratio, however it is achieved. An evicted item is one that cannot be hit. Early eviction is not ipso facto good for performance; only with regard to some access patterns where early eviction of the right items improves retention of other items that yield good hits.

Re:Sieve is simpler than LRU by vidarh on Wednesday January 03, 2024 @01:46PM (#38857965)
Early eviction is fine for performance if the average eviction you get is good enough that any improvement in eviction you get from a more optimal choice of distance is outweighed by the effort in picking the insertion point.

You really need to measure, because this will depend on your workload, how fast your implementation is relative to the cost of a miss, what the ratio of "likely to never be requested again" to potentially long-lived objects are, etc.

Re:Sieve is simpler than LRU by kazinator on Wednesday January 03, 2024 @05:05PM (#38860525)
The roving Insert point is harder to implement, due to the reversed case:

  [ ][ ][I][ ][ ][H][ ][ ]
         ^^^^^^^^
                 `these items move right

  [ ][ ][H][ ][I][ ][ ][ ]
   ^^^^        ^^^^^^^  ^- this item moves to the top (left most slot)
    |                 `--- these items move right
     ` these items move right
If we think about doing it in hardware, like for each set of a set-associative L1 or L2 cache, it's a hindrance.
Re:Sieve is simpler than LRU by vidarh on Wednesday January 03, 2024 @06:03PM (#38861016)
I'd just have implemented it as a ring instead of a list and insertion at any point is identical, but my point was exactly that unless your workload specifically fits insert at the "hand" better than the current algorithm, you will need to account for the cost of whatever complexity you add in order to move the insert point.

> If we think about doing it in hardware, like for each set of a set-associative L1 or L2 cache, it's a hindrance.

I don't see why I would think about doing it in hardware.

Re:Sieve is simpler than LRU by 1a1a11a on Wednesday January 03, 2024 @05:19PM (#38860655)
I totally agree with you, this is not designed for hardware... and as others mentioned RRIP might be better for set-associative caches
Re:Sieve is simpler than LRU by vidarh on Wednesday January 03, 2024 @06:57PM (#38861428)
With respect to my other reply mentioning a ring instead of a linked list, here's a quick and dirty Ruby port because I couldn't resist that uses a ring with a single dummy head node to simplify the logic:

https://gist.github.com/vidarh/b72615c16673cb38630cab5148190...

You'll note it avoids a bunch of conditionals in add_to_head, remove_node and evict because the wraparound happens automatically and the next/prev pointers are never nil/None

I've not verified it matches the behaviour of yours exactly other than adding a handful of extra access calls and comparing the state.

If you're not familiar with Ruby, the one particularly dirty trick I'm using is that in Ruby you can define methods directly on a specific object, so by defining the method "@head.visited" to always return true, the eviction will just try to set it to false and move straight over it every run, cutting a conditional. You could do a ring without a dummy @head to but it makes the insertion/deletion logic more annoying.

Re:Sieve is simpler than LRU by 1a1a11a on Thursday January 04, 2024 @02:28PM (#38871174)
it looks like the ring is implemented using a linked list?
Re:Sieve is simpler than LRU by vidarh on Friday January 05, 2024 @02:43AM (#38876717)
Yes, I wanted to change as little as possible to illustrate why you almost always want to close your lists into a ring when using a linked list ;)

It simplifies a lot of algorithms to be able to guarantee no null pointers. It's a very old trick. You trade a lot of conditionals and a risk of null pointers dereference for a smaller risk of non-termination (failure to check for a null pointer is always bad; failure to check for the end is only a problem if the end of the list is actually your termination condition).

You could do a ring with an array and modulus too, but then you end up copying the whole thing to open up slots to insert to maintain the property of your insertion point being separate from the hand.

That may or may not do worse, but likely to depend on cache size and implementation language.

What you'd almost certainly want in a production implementation anyway would in any case be to do away with constantly allocating nodes and just move them to a free list and reuse to avoid dynamic allocation overhead and garbage collection.

Re:Sieve is simpler than LRU by vidarh on Friday January 05, 2024 @07:06AM (#38878268)
I added a second, singly-linked ring version to the gist I linked above. It doesn't save much, as to be able to unlink the hand you need to keep a pointer to the node adjacent to the hand (I changed "direction" in the naming because it feels weird to have a singly linked list with just "prev" pointers, but that doesn't matter), but it does mean a pointer less for each Node and from experience a lot of people struggle to get the pointer updates right for doubly linked lists, and that is a lot harder to get wrong when singly linked...
Re:Sieve is simpler than LRU by 1a1a11a on Saturday January 06, 2024 @11:26PM (#38898424)
Got it. Make sense, I like the reason why you want to make it circular. :)