ในช่วงหลายปีที่ผ่านมา ผู้ที่เกี่ยวข้องกับวิทยาการคอมพิวเตอร์และวิศวกรรมศาสตร์ได้ทำงานอย่างหนักเพื่อเพิ่มประสิทธิภาพในลักษณะต่างๆ เนื่องจากเราอาศัยอยู่ในโลกที่มีทรัพยากรจำกัด มนุษยชาติจึงพยายามหาทางลดต้นทุนให้เหมาะสมและเร่งความเร็วให้กับทุกสิ่งอยู่เสมอ
ในด้านวิศวกรรมซอฟต์แวร์ ฉันขอยืนยันว่าแนวทางที่ได้รับความนิยมมากที่สุดในการปรับปรุงประสิทธิภาพคือการแคช แม้ว่าการแคชจะมีแอปพลิเคชันต่างๆ มากมาย ขึ้นอยู่กับสาขาวิศวกรรมซอฟต์แวร์ แนวคิดเบื้องหลังการแคชนั้นค่อนข้างง่าย: จัดเก็บข้อมูลที่มักจำเป็น/ใช้ในโครงสร้าง/พื้นที่จัดเก็บที่รวดเร็ว เพื่อให้สามารถเรียกค้นได้เร็วมาก
ที่จริงแล้ว แคชจะต้องรวดเร็วในสองมิติ:
- ตรวจสอบให้แน่ใจว่าคำขอไฟล์จำนวนมากไปที่ไฟล์นั้น (การเข้าถึงแคช) ไม่ใช่ผ่านเครือข่ายหรือไปยังหน่วยความจำหลัก (พลาดแคช)
- ค่าใช้จ่ายในการใช้งานควรมีน้อย: การทดสอบความเป็นสมาชิกและการตัดสินใจว่าจะแทนที่ไฟล์เมื่อใดควรดำเนินการโดยเร็วที่สุด
ในบทความนี้ เราจะเน้นในส่วนที่สอง: การนำแนวทางการใช้งานเฉพาะไปใช้กับแคชที่ใช้บ่อยน้อยที่สุด และทำให้การทดสอบความเป็นสมาชิกและอัลกอริธึมการขับไล่มีประสิทธิภาพ นอกจากนี้ เรายังจะกล่าวถึงข้อมูลพื้นฐานและสำรวจว่ารูปแบบการแคชดังกล่าวจะมีประโยชน์ตรงจุดใด
พื้นฐาน
LFU เป็นอัลกอริธึมการแคชซึ่งรายการที่ใช้น้อยที่สุดในแคชจะถูกลบออกทุกครั้งที่ถึงขีดจำกัดความจุของแคช ซึ่งหมายความว่าสำหรับทุกรายการในแคชของเรา เราต้องติดตามความถี่ในการใช้งาน เมื่อแคชเกินความจุ อัลกอริธึมการกำจัดจะถูกเรียกใช้ โดยจะต้องเลือกและหมดอายุ (ลบ) รายการออกจากแคช
หากคุณเคยใช้แคช LFU คุณอาจเคยลองใช้โครงสร้างข้อมูล min-heap เนื่องจากโครงสร้างดังกล่าวจัดการการแทรก การลบ และการอัปเดตในความซับซ้อนของเวลาลอการิทึม ในบทความนี้ เราจะดูแนวทางอื่นในการนำไปปฏิบัติ
แต่ก่อนที่เราจะนำไปใช้จริง เรามาดูกันว่ากรณีใดที่ LFU ดีกว่าทางเลือกอื่น
ที่ที่ LFU ส่องแสง
ลองนึกภาพแคชเนื้อหาบน CDN โดยที่เนื้อหาจะถูกแคชตามรูปแบบการใช้งาน ดังนั้น เมื่อผู้ใช้ขอรูปภาพบางส่วนสำหรับหน้าเว็บที่พวกเขาขอ CDN นี้จะเพิ่มลงในแคชเพื่อให้ผู้ใช้รายอื่นได้รับรูปภาพเร็วขึ้น
ตัวอย่างเช่น รูปภาพหนึ่ง (เนื้อหา) ดังกล่าวคือโลโก้ของเว็บไซต์ คุณลองจินตนาการดูว่ามีการขอโลโก้ของ Google ในทุกผลิตภัณฑ์ของตนวันละกี่ครั้ง ฉันอยากจะค้นหาตัวเลขนั้นจริงๆ แต่สำหรับตอนนี้ เราอาจตกลงกันว่าตัวเลขนั้นใหญ่มาก
แคชเนื้อหาดังกล่าวเป็นกรณีการใช้งานที่สมบูรณ์แบบสำหรับแคช LFU อัลกอริธึมการลบแคช LFU จะไม่ลบเนื้อหาที่มีการเข้าถึงบ่อยครั้ง ในความเป็นจริง ในแคชดังกล่าว โลโก้ของ Google จะถูกแคชตลอดไป ในทางตรงกันข้าม หากมีรูปภาพใดที่จะเข้าถึงได้เนื่องจากหน้า Landing Page ใหม่ของผลิตภัณฑ์ใหม่ที่อยู่หน้าแรกของ Reddit, Slashdot & Hackernews เมื่อพายุกระแสเกินผ่านไป เนื้อหาจะถูกไล่ออกเร็วขึ้นเนื่องจากการเข้าถึง ความถี่จะลดลงอย่างมาก แม้ว่าในช่วงไม่กี่วันที่ผ่านมาจะมีการเข้าถึงหลายครั้งก็ตาม
ดังที่คุณอาจสังเกตเห็นแล้วว่า วิธีการกำจัดแคชนี้มีประสิทธิภาพมากในกรณีที่รูปแบบการเข้าถึงของออบเจ็กต์ที่แคชไม่เปลี่ยนแปลงบ่อยครั้ง แม้ว่าแคช LRU จะขับไล่สินทรัพย์ที่ไม่สามารถเข้าถึงได้เมื่อเร็วๆ นี้ แต่แนวทางการขับไล่ LFU จะขับไล่สินทรัพย์ที่ไม่ต้องการอีกต่อไปหลังจากที่กระแสเกินจริงได้ยุติลง
การใช้แคช LFU
ตอนนี้เรามาดูเนื้อของมันกันดีกว่า ดังที่เราได้กล่าวไว้ก่อนหน้านี้ แทนที่จะมองว่า min-heap เป็นโครงสร้างข้อมูลที่เป็นไปได้ที่จะสำรองแคช LFU ของเรา เราจะพิจารณาแนวทางที่ดีกว่า
ในความเป็นจริง ในปี 2010 กลุ่มนักวิจัย Prof. Ketan Shah, Anirban Mitra และ Dhruv Matani ตีพิมพ์บทความชื่อ “อัลกอริทึม O(1) สำหรับการนำโครงการกำจัดแคช LFU ไปใช้” (คุณสามารถตรวจสอบได้ ที่นี่) ซึ่ง พวกเขาอธิบายการใช้งานแคช LFU ที่มีความซับซ้อนรันไทม์ O(1)
สำหรับการดำเนินการทั้งหมด ซึ่งรวมถึงการแทรก การเข้าถึง และการลบ (การขับไล่)
ที่นี่ ฉันจะแสดงให้คุณเห็นว่าเราสามารถนำแคชนี้ไปใช้ได้อย่างไร และแนะนำคุณตลอดขั้นตอนการใช้งาน
โครงสร้างข้อมูล
ไม่ มันจะไม่ใช่ต้นไม้สีแดงดำของแฟรงเกนสไตน์ ในความเป็นจริงแล้ว มันคือสองรายการที่มีการเชื่อมโยงแบบทวีคูณและตารางแฮช ใช่แล้ว นั่นคือทั้งหมดที่
เพื่อให้เข้าใจพื้นฐานของการนำ LFU ไปใช้นี้ ลองดูที่รายการที่เชื่อมโยงและตารางแฮชเป็นกราฟิก ก่อนที่เราจะดูกราฟิกจริง เราต้องเข้าใจว่าตารางแฮชและรายการลิงก์จะถูกนำไปใช้อย่างไร
ตารางแฮชจะจัดเก็บรายการทั้งหมดด้วยคีย์ที่ได้รับการประมวลผลผ่านอัลกอริธึมการแฮช (สำหรับจุดประสงค์ของเราเราสามารถทำให้มันง่ายได้) และค่าจะเป็นรายการจริง:
รายการที่เชื่อมโยงนั้นซับซ้อนกว่าเล็กน้อย อันแรกจะเป็น "รายการความถี่" ซึ่งจะมีความถี่ในการเข้าถึงทั้งหมด แต่ละโหนดในรายการนี้จะมีรายการซึ่งจะประกอบด้วยรายการทั้งหมดที่เข้าถึงด้วยความถี่ที่สอดคล้องกัน นอกจากนี้ แต่ละรายการในรายการจะมีตัวชี้ไปยังบรรพบุรุษในรายการความถี่:
หากเราดูตัวอย่างกราฟิกของเราด้านบน เราจะสังเกตเห็นว่ามีการเข้าถึงรายการ A
, B
, C
และ D
1 ครั้ง มีการเข้าถึงรายการ E
และ F
4 ครั้งและต่อๆ ไป เส้นสีน้ำเงินคือตัวชี้ที่แต่ละรายการในรายการมีถึงบรรพบุรุษในรายการความถี่
แล้วจะเกิดอะไรขึ้นหากรายการ E
เข้าถึงได้อีกครั้ง? มาดูขั้นตอนกัน: 1. การดึงไอเท็มจากตารางแฮชเป็นเรื่องง่าย (และปรับขนาดได้ดี O(1)
) 2. เราจะเข้าถึงตัวชี้ frequencyParent
ของไอเท็ม ซึ่งเราจะสามารถตรวจสอบความถี่ถัดไปในรายการได้ 3. ถ้า มีความถี่ใหม่อยู่ (เช่น 8
) เราจะพุชให้เป็นรายการแรกของรายการภายใต้โหนดความถี่ 8
4. หากไม่มีความถี่ใหม่ เราจะสร้างโหนดความถี่ 8
และจะเพิ่ม E
ลงในรายการ
แค่นั้นแหละ. การดึงข้อมูลรายการและการรีเฟรชความถี่ของรายการคือ O(1)
ก่อนที่เราจะใช้งานอัลกอริธึมการเข้าถึง เรามาสร้างประเภทพื้นฐานที่เราต้องใช้เพื่อให้งานนี้ใช้งานได้ก่อน
ประเภท
ดังที่เราได้กล่าวไว้ก่อนหน้านี้ เราจำเป็นต้องจำลองประเภทที่จำเป็นซึ่งจะเป็นแกนหลักของแคชของเรา
struct
ตัวแรกจะเป็น CacheItem
นี่จะเป็นรายการจริงที่จะถูกเก็บไว้ในแคช:
type CacheItem struct {
key string // Key of entry
value interface{} // Value of item
frequencyParent *list.Element // Pointer to parent in cacheList
}
ประกอบด้วย key
ซึ่งเราสามารถค้นหาได้ในตารางแฮช value
ซึ่งเป็นรายการแคชจริง และตัวชี้ frequencyParent
ไปยังตัวชี้ในรายการความถี่
struct
ถัดไปคือ FrequencyItem
ซึ่งแสดงถึงแต่ละรายการในรายการความถี่ ประกอบด้วยชุดของรายการซึ่งจะเป็นชุดของ CacheItem
พอยน์เตอร์ เราจะใช้ map
เพื่อจัดเก็บเพื่อให้เราสามารถถือเป็นชุดซึ่งมีเฉพาะรายการที่ไม่ซ้ำเท่านั้น:
type FrequencyItem struct {
entries map[*CacheItem]byte // Set of entries
freq int // Access frequency
}
struct
สุดท้ายที่เราจะต้องมีเพื่อให้แคชทำงานได้อย่างราบรื่นก็คือ Cache
นั่นเอง:
type Cache struct {
bykey map[string]*CacheItem // Hashmap containing *CacheItems for O(1) access
freqs *list.List // Linked list of frequencies
capacity int // Max number of items
size int // Current size of cache
}
Cache
จะมีแฮชแมปที่เรียกว่า bykey
(การตั้งชื่อมาจากกระดาษที่ลิงก์ด้านบน) รายการความถี่ที่เรียกว่า freqs
ความจุสูงสุดของแคชที่เรียกว่า capacity
และ size
ของแคชซึ่งแสดงถึงจำนวนรายการที่แคชไว้ที่ใดก็ตาม ช่วงเวลา.
ใหม่ ตั้งค่าและรับ
มาดูฟังก์ชันสามประการแรกที่จำเป็นในการทำให้แคชของเราทำงานได้ อันแรกคือฟังก์ชันคอนสตรัคเตอร์เล็กน้อย:
func New() *Cache {
cache := new(Cache)
cache.bykey = make(map[string]*CacheItem)
cache.freqs = list.New()
cache.size = 0
cache.capacity = 100
return &c
}
ตัวสร้าง New
จะสร้างโครงสร้าง Cache
ใหม่และจะตั้งค่าเริ่มต้นทั้งหมดให้กับมัน ในกรณีที่คุณสงสัยว่า list.New()
ทำงานอย่างไร: สำหรับรายการความถี่ เราจะใช้แพ็คเกจ container/list
ของ Go ซึ่งมีการใช้งานรายการลิงก์ที่เรียบร้อย คุณสามารถตรวจสอบ "เอกสารประกอบ" เพื่อดูรายละเอียดเพิ่มเติมได้
ฟังก์ชันที่สองซึ่งจะนำไปใช้กับ Cache
คือฟังก์ชัน Set
:
func (cache *Cache) Set(key string, value interface{}) {
if item, ok := cache.bykey[key]; ok {
item.value = value
// Increment item access frequency here
} else {
item := new(CacheItem)
item.key = key
item.value = value
cache.bykey[key] = item
cache.size++
// Eviction, if needed
// Increment item access frequency
}
}
ฟังก์ชันจะใช้คีย์แคชและค่า/รายการจริงที่จะแคชเป็นอาร์กิวเมนต์ จากนั้นจะตรวจสอบว่ารายการนั้นถูกแคชไว้แล้วหรือไม่ หากถูกแคชไว้ ระบบจะอัปเดตค่าในรายการ มิฉะนั้น มันจะสร้าง CacheItem
ใหม่ซึ่งจะห่อหุ้มค่าจริง มันจะตั้งค่า key
มันจะเพิ่มรายการในแฮช bykey
และจะเพิ่ม size
ของแคช
ตอนนี้ ในตรรกะทั้งสองสาขา ฉันได้เพิ่มความคิดเห็นสำหรับส่วนที่ขาดหายไป: 1. Cache
จะต้องรู้วิธีเพิ่มความถี่ในการเข้าถึงสำหรับ aCacheItem
แต่เรายังไม่ได้ดำเนินการดังกล่าว 2. Cache
จะต้องรู้วิธียกเลิกรายการตามความถี่ในการเข้าถึง หาก size
ถึง capacity
เราจะเก็บความคิดเห็นเหล่านี้ไว้จนกว่าเราจะใช้ฟังก์ชัน increment
และ evict
ฟังก์ชันที่สามที่ Cache
จะได้รับคือ Get
- เข้าถึงรายการด้วยคีย์จาก hashtable และส่งคืน:
func (cache *Cache) Get(key string) interface{} {
if e, ok := cache.bykey[key]; ok {
// Increment acess frequency here
return e.value
}
return nil
}
ไม่มีเวทย์มนตร์ที่นี่เช่นกัน เราจะตรวจสอบว่า bykey
hashtable มีค่าที่มีอาร์กิวเมนต์ key
หรือไม่ และเราจะส่งคืนค่านั้นหากมี มิฉะนั้นเราจะส่งคืน nil
ที่นี่ เช่นเดียวกับใน Set
เราจะทิ้งความคิดเห็นตัวยึดตำแหน่งไว้ โดยที่เราต้องเพิ่มการเรียกใช้ฟังก์ชันการเพิ่มความถี่เมื่อเรานำไปใช้
กำลังอัปเดตความถี่ในการเข้าถึง
ดังที่เราเห็นแล้วว่า ทุกครั้งที่ดำเนินการเข้าถึงไปยังแคช เราจะต้องอัปเดตความถี่ในการเข้าถึงของรายการที่เข้าถึง
มาดูขั้นตอนที่ฟังก์ชัน Increment
ของเราต้องทำกัน อันดับแรก เพื่อให้ไอเท็มหมดอายุ เราจะต้องตัดสินใจว่าไอเท็มนี้เป็นส่วนหนึ่งของตารางแฮชและรายการความถี่อยู่แล้วหรือไม่ หากเป็นเช่นนั้น เราจะต้องค้นหาค่าความถี่ใหม่และตำแหน่งความถี่ถัดไป (โหนด) ในรายการความถี่
ประการที่สอง เราจะต้องพิจารณาว่าสำหรับความถี่ใหม่นั้นมีโหนดอยู่ในรายการความถี่อยู่แล้วหรือไม่ หากมี เราจะต้องเพิ่มรายการดังกล่าวลงในรายการ entries
และกำหนดความถี่ในการเข้าถึงใหม่ (ซึ่งก็คือความถี่ในการเข้าถึงปัจจุบัน + 1) หากไม่มี เราจะต้องสร้างโหนดความถี่ใหม่ในรายการความถี่ (และตั้งค่าเริ่มต้นที่มีสติทั้งหมด) จากนั้นเพิ่มรายการนั้นลงในรายการ entries
ประการที่สาม เมื่อเราตรวจพบ FrequencyParent
แล้ว ฟังก์ชันของเราจะต้องตั้งค่าพาเรนต์ใหม่เป็น item
ที่กำลังเพิ่มขึ้น และเพิ่มลงในรายการพาเรนต์ที่ entries
ในขั้นตอนสุดท้าย ฟังก์ชัน increment
จะลบรายการออกจาก entries
ของโหนดความถี่เก่า (frequencyParent
)
นี่คือรหัสใน Golang:
func (cache *Cache) increment(item *CacheItem) {
currentFrequency := item.frequencyParent
var nextFrequencyAmount int
var nextFrequency *list.Element
if currentFrequency == nil {
nextFrequencyAmount = 1
nextFrequency = cache.freqs.Front()
} else {
nextFrequencyAmount = currentFrequency.Value.(*FrequencyItem).freq + 1
nextFrequency = currentFrequency.Next()
}
if nextFrequency == nil || nextFrequency.Value.(*FrequencyItem).freq != nextFrequencyAmount {
newFrequencyItem := new(FrequencyItem)
newFrequencyItem.freq = nextFrequencyAmount
newFrequencyItem.entries = make(map[*CacheItem]byte)
if currentFrequency == nil {
nextFrequency = cache.freqs.PushFront(newFrequencyItem)
} else {
nextFrequency = cache.freqs.InsertAfter(newFrequencyItem, currentFrequency)
}
}
item.frequencyParent = nextFrequency
nextFrequency.Value.(*FrequencyItem).entries[item] = 1
if currentFrequency != nil {
cache.remove(currentFrequency, item)
}
}
ย้อนกลับไปที่แผนภาพดั้งเดิมของเราของรายการความถี่และรายการ และดำเนินการผ่านการเพิ่มรายการ E
ขั้นตอนแรกที่ฟังก์ชัน increment
ของเราจะดำเนินการคือการจัดสรรตัวชี้ไปยังโหนด 4
(frequencyParent
) และ value
(ซึ่งก็คือ 4
) เนื่องจากโหนด 4
มีอยู่ในรายการ มันจะค้นหาโหนดถัดไปในรายการความถี่ ซึ่งในกรณีของเราคือโหนด 7
เมื่อทราบแล้วว่าความถี่ใหม่สำหรับโหนด E
ควรเป็น 5
ไม่ใช่ 7
ความถี่ดังกล่าวจะเพิ่มโหนดความถี่ใหม่ในรายการ ระหว่างโหนด 4
และ 7
:
เมื่อเพิ่มโหนด 5
ลงในรายการแล้ว ฟังก์ชันจะตั้งค่าเริ่มต้นที่จำเป็นสำหรับโหนดเพื่อให้ทำงานได้อย่างถูกต้อง จากนั้นจะตั้งค่าตัวชี้ของ E
เป็น frequencyParent
ใหม่ (โหนด 5
):
ในขั้นตอนสุดท้าย จะใช้ item
ซึ่งมีประเภท *CacheItem
และจะเพิ่มลงในรายการ entries
ในขณะที่ลบออกจากรายการ entries
จาก frequencyParent
ก่อนหน้า:
มาดูขั้นตอนในการลบ CacheItem
ออกจากรายการ entries
ของ FrequencyItem
กำลังลบรายการ
เมื่อเราทราบโหนดในรายการที่เราต้องการลบออก เราก็สามารถลบรายการออกจากรายการ entries
และลบ FrequencyItem
ออกจากรายการความถี่โดยสมบูรณ์หาก entries
ว่างเปล่า:
func (cache *Cache) Remove(listItem *list.Element, item *CacheItem) {
frequencyItem := listItem.Value.(*FrequencyItem)
delete(frequencyItem.entries, item)
if len(frequencyItem.entries) == 0 {
cache.freqs.Remove(listItem)
}
}
การขับไล่
ชิ้นส่วนสุดท้ายของปริศนาคือการขับไล่ หรืออีกนัยหนึ่งคือ การลบรายการที่ใช้บ่อยน้อยที่สุดออกเมื่อแคชถึงความจุสูงสุด
เราต้องรู้ว่าเราต้องการขับไล่สิ่งของจำนวนเท่าใด โดยปกติแล้ว แคชของเราจะมีขอบเขตต่ำและสูง ดังนั้นเมื่อถึงขอบเขตสูง เราจะลบรายการออกจนกว่าจะถึงขอบเขตต่ำ ในกรณีของเรา เราจะกำจัดรายการจำนวนหนึ่งซึ่งฟังก์ชัน Evict
จะถือเป็นอาร์กิวเมนต์
ฟังก์ชั่นจะเริ่ม “เดินผ่าน” รายการความถี่ตั้งแต่ต้นจนจบ เนื่องจากรายการความถี่เรียงลำดับจากน้อยไปหามาก จึงจะเริ่มลบ entries
ออกจากโหนดความถี่แรกเป็นต้นไป จนกว่าจะลบรายการได้มากเท่ากับจำนวนที่ส่งผ่านเข้ามา
หากโหนดความถี่ไม่มี entries
เนื่องจากการไล่ออก ฟังก์ชัน Evict
จะต้องลบโหนดความถี่ออกจากรายการความถี่ด้วย มันจะทำเช่นนั้นโดยการเรียกใช้ฟังก์ชัน Remove
ด้วยวิธีนี้การขับไล่จะไม่ทิ้งขยะไว้ข้างหลัง
นี่คือรหัสของสิ่งที่เราอธิบายไว้ข้างต้น:
func (cache *Cache) Evict(count int) {
for i := 0; i < count; {
if item := cache.freqs.Front(); item != nil {
for entry, _ := range item.Value.(*FrequencyItem).entries {
if i < count {
delete(cache.bykey, entry.key)
cache.Remove(item, entry)
cache.size--
i++
}
}
}
}
}
กลับไปที่การตั้งค่าและรับ
ในตอนต้นของบทความนี้ เราได้ใช้ฟังก์ชัน Set
และ Get
สิ่งหนึ่งที่เรายังไม่มีในตอนนั้นคือฟังก์ชัน Evict
และ increment
ดังนั้นเราจึงสามารถใช้ฟังก์ชันเหล่านี้ได้ตามนั้น มาเพิ่มการร้องขอของพวกเขากัน
ความถี่ในการเข้าถึงที่เพิ่มขึ้น
ในฟังก์ชัน Get
หากเราพบรายการในตารางแฮช bykey
เราจำเป็นต้องเพิ่มความถี่ในการเข้าถึงรายการนั้นก่อนที่เราจะดำเนินการส่งคืน value
:
func (cache *Cache) Get(key string) interface{} {
if e, ok := cache.bykey[key]; ok {
cache.increment(e)
return e.value
}
return nil
}
ด้วยการเปลี่ยนแปลงนี้ Cache
จะเพิ่มความถี่ของรายการนั้นก่อนที่เราจะส่งคืน แต่เราลืมอะไรบางอย่างไปหรือเปล่า? นอกจากนี้ ฟังก์ชัน Set
ยังทำให้สามารถเข้าถึงรายการแคชเมื่อแคชรายการเหล่านั้นจริงๆ ซึ่งหมายความว่าเมื่อรายการถูกแคชไว้ รายการนั้นจะถูกเพิ่มลงในรายการความถี่ทันที ใต้โหนดที่มีค่า 1
:
func (cache *Cache) Set(key string, value interface{}) {
if item, ok := cache.bykey[key]; ok {
item.value = value
cache.increment(item)
} else {
item := new(CacheItem)
item.key = key
item.value = value
cache.bykey[key] = item
cache.size++
// Eviction, if needed
cache.increment(item)
}
}
การขับไล่ภายหลังการบวก
ฟังก์ชัน Set
ช่วยให้ผู้ใช้ LFU Cache
ของเราแคชรายการต่างๆ ได้มากขึ้น องค์ประกอบสำคัญของแคชคือควรรู้วิธีกำจัดรายการต่างๆ (เพิ่มพื้นที่ว่าง) เมื่อมีการเพิ่มรายการใหม่ลงในแคช สำหรับแคช LFU รายการที่ใช้บ่อยน้อยที่สุดจะต้องถูกลบออกเมื่อแคชมีความจุเต็ม
ก่อนอื่นมาเพิ่มฟังก์ชันที่จะส่งคืน bool
หากแคชถึงความจุสูงสุดแล้ว:
func (cache *Cache) atCapacity() bool {
return cache.size >= cache.capacity
}
ฟังก์ชันนี้เรียบง่าย: ตรวจสอบว่า size
ปัจจุบันของ Cache
นั้นใหญ่กว่าหรือเท่ากับ capacity
หรือไม่
ตอนนี้ เรามาลองใช้ฟังก์ชัน Set
กันดีกว่า เมื่อเราตั้งค่ารายการใหม่ในแคชแล้ว เราจะต้องตรวจสอบว่าแคชถึงความจุแล้วหรือไม่ จากนั้นจึงกำจัดรายการจำนวนหนึ่งออกจากแคช
เพื่อความง่าย เราจะลบออกเพียง 10 รายการทุกครั้งที่ใช้ความจุสูงสุด:
func (cache *Cache) Set(key string, value interface{}) {
if item, ok := cache.bykey[key]; ok {
item.value = value
cache.increment(item)
} else {
item := new(CacheItem)
item.key = key
item.value = value
cache.bykey[key] = item
cache.size++
if cache.atCapacity() {
cache.Evict(10)
}
cache.increment(item)
}
}
ด้วยการเปลี่ยนแปลงนี้ หาก ณ จุดใดก็ตามการเพิ่มรายการถึงความจุของแคช แคชจะลบรายการที่ใช้บ่อยน้อยที่สุด
หากคุณต้องการดูโค้ดทั้งหมดของบทความนี้ คุณสามารถดู ส่วนสำคัญนี้
LFU เป็นแผนการขับไล่ที่น่าสนใจ โดยเฉพาะอย่างยิ่งเมื่อเปรียบเทียบกับ LRU ในความคิดของฉัน เนื่องจากลักษณะที่แหวกแนว แม้ว่าแอปพลิเคชันจะมีจำกัด แต่อัลกอริทึมและโครงสร้างข้อมูลสำรองที่อธิบายไว้ในบทความที่ใช้ในบทความนี้ก็น่าสนใจเนื่องจากความสามารถในการปรับขนาดของแนวทางนี้
หากเรากลับมาดู บทความ ที่เรากล่าวถึงในตอนต้นของบทความ เราจะเห็นว่าแม้ว่า LFU จะไม่ใช่ข่าว แต่เดิมมีการใช้ min-heap ซึ่งมีเวลาลอการิทึมสำหรับการแทรก การค้นหา และการลบ สิ่งที่น่าสนใจในบทความนี้ ผู้เขียนอธิบายว่าแนวทางที่พวกเขาเสนอมีความซับซ้อนของเวลา O(1)
สำหรับการดำเนินการแต่ละรายการ (การแทรก การค้นหา และการลบ) เนื่องจากการดำเนินการจะขึ้นอยู่กับตารางแฮช นอกจากนี้ รายการที่เชื่อมโยงไม่ได้เพิ่มความซับซ้อนของเวลาใดๆ เนื่องจากเราไม่ได้สำรวจรายการ ณ จุดใดๆ - เราเพียงแค่เพิ่มหรือลบโหนดในรายการเหล่านั้นหากจำเป็น (ซึ่งเป็นการดำเนินการ O (1))
ในการปิด
ในบทความนี้ เราได้อธิบายพื้นฐานของแคช LFU แล้ว เราได้กำหนดเกณฑ์ชี้วัดประสิทธิภาพที่สำคัญที่สุด (อัตราส่วนการเข้าชมต่อพลาด สมาชิกภาพ และความเร็วในการไล่ออก) เราพบว่าแม้ว่าจะไม่ใช่รูปแบบการแคชที่ใช้กันอย่างแพร่หลาย แต่ก็มีประสิทธิภาพมากในบางกรณี
จากนั้นเราก็ดำเนินการต่อไป โดยใช้แนวทางที่ปรับขนาดได้ค่อนข้างดีในแง่ของความซับซ้อนของเวลา เราเห็นความซับซ้อนของการนำอัลกอริธึมการขับไล่และการเพิ่มความถี่ไปใช้ ในตอนท้าย เราได้สำรวจเพิ่มเติมว่าแนวทางที่เราใช้ในการนำไปใช้นั้นปรับขนาดได้อย่างไร
เผยแพร่ครั้งแรกที่ ieftimov.com เมื่อวันที่ 27 กุมภาพันธ์ 2019