【反饋】緩存命中率控制系統

前言

需要多大的緩存才能維持特定的命中率?

Cache Hit Rate 緩存命中率指的是某處內存被緩存數據後被訪問到(命中)的概率,即一段時間內命中數與訪問數的比例。


簡介PID

【反饋】這個專欄現在是把《企業級編程與控制理論》中的一些知識寫出來,其實就是模擬系統、建模、信號與系統相關知識加上Python實現不同案列的模擬系統的知識。一個反饋系統不能避免被討論的就是PID了,即比例積分微分三相調節器。這裏再簡單介紹下我現在所理解的PID。
在這裏插入圖片描述
爲了方便理解,做一個簡圖。藍色的線是設定的目標輸出值,紅色的是系統實際的輸出值,二者之差是error,其乘以取樣間隔就是此點附近的矩形面積——兩條線之間的矩形。兩個取樣點間的error變化率就是上式中的微分項。u是實際系統輸入。假設系統爲y=nx,那麼u就是x,y是實際輸出,error=r-y

因爲不知道系統內部的n是多少,所以只能在外面瞎猜!假設這真的就是個線性系統,那麼只要輸入正確的x,就一定能得到想要的y。如果是曲線的方程的話,取一小段也可以當做線性。怕的就是如果輸入同樣一個數得到不同的輸出,那就麻煩點了,這種情況可以考慮概率,某個輸入使輸出的最有可能滿足目標就行!!!

當實際輸出滿足設定的目標時,error=0,則比例項爲零,但是隻有一個取樣值滿足還不能確定輸出就穩定了,可能只是兩條曲線交叉了一下而已!所以需要再進行取樣,兩次取樣間的error=0變化率如果爲零,說明輸出穩定了。但是!!!僅僅這樣還是不行,輸出穩定不一定就是輸出和目標一樣了——可能兩條線平行了!那你也會問,不是說好了目標和輸出一樣了嗎???答案是式子中的比例項不是有個係數嗎!如果比例係數比較小,那麼細小的差別就會被忽略,即接近零,如果比例係數太大,輸入又太大。

  \;
然後我們看積分項,積分對應的就是兩條曲線之間的面積,如果面積不變了,那麼也能說明error=0,此時積分項爲某個特定的常數,這個常數輸入系統就有可能得到目標值!但是!!!此時仍然沒有解決上面這個問題,可能兩條曲線平行了。當兩條曲線平行時,error值太小以至於需要好一會兒才能在輸出端感覺到這種差異性。所以此時需要加上反應快一些的比例項……

  • 介紹到這裏並沒有得出PID就能滿足所有系統反饋的情況!都是大概滿足要求,並且上面我所寫的式子到文章後面還有優化!

模擬控制系統框架

製作一個feedback包。

#__init__.py
__name__="feedback package with PID and some filter"
__author__="應急食品派蒙"

#全局變量
__all__=["deltaT","yForPlot"]

#離散系統取樣間隔,默認1s
deltaT=1
#記錄實際輸出命中率的變量,用來繪圖
yForPlot=[]

#Component.py

'''
組件的基類
'''
class Component:
	#封裝了組件的動態參數,每個時間幀調用一次這個函數
	def work(self,arr):
		return arr

	#返回一組表示組件內部狀態的任意字符串
	def monitoring(self):
		return ""

#PID_Controller.py
import feedback
import feedback.Component


'''
	PID係數
	kp比例係數
	ki積分系數
	kd微分系數
	i 積分項
	d 微分項
	prevError 前一時間幀的偏移
'''



class PID_Controller(feedback.Component.Component):

	def __init__(self,kp,ki,kd=0):
		self.kp,self.ki,self.kd=kp,ki,kp
		self.i=0
		self.d=0
		self.prevError=0
	def work(self,error):
		self.i+=feedback.deltaT*error
		self.d=(error-self.prevError)/feedback.deltaT
		self.prevError=error
		return self.kp*error +self.ki*self.i + self.kd*self.d
#ADV_Controller.py
import feedback
import feedback.Component


'''
ADV_Controller提供了比PID_Controller更高級的實現
多了兩個功能:一個是防止積分器飽和的“限值”,防止積分器飽和。
如果控制器輸出超過構造函數所指定的限制,積分項在下一時間幀
中不會更新。限值這一任務由控制器下面的“執行器”來決定

另一個是爲微分計算的過濾器。
默認情況下,不會對微分部分做平滑處理,但是如果給平滑參數
傳遞小於一的正數時,將調用簡單的遞歸過濾器(單一指數
平滑),以平滑微分部分的貢獻。


	PID係數
	kp比例係數
	ki積分系數
	kd微分系數
	i 積分項
	d 微分項
	prevError 前一時間幀的偏移
	clamped   是否限值
	clamp_low 限值的低值
	clamp_high 限值的高值
	ratio     當前微分佔之後微分的比率
	
	
'''



class ADV_Controller(feedback.Component.Component):

	#默認PI調節,默認沒有微分過濾器
	def __init__(self, kp, ki, kd=0,clampLimit=(-1e10,1e10),ratio=1):
		self.kp, self.ki, self.kd = kp, ki, kp
		self.i = 0
		self.d = 0
		self.prevError = 0
		self.clamped=True
		self.clamp_low,self.clamp_high=clampLimit
		self.ratio=ratio

	def clamp(self,arr):
		return self.clamp_low<arr<self.clamp_high

	def work(self, error):
		#如果沒有限值(clamped=False),積分項就保持不變
		if self.clamped:
			self.i+=feedback.deltaT*error

		#微分項除了此時間幀的微分外,還增加了之前的微分項
		self.d=(self.ratio*(error-self.prevError)/feedback.deltaT + (1.0-self.ratio)*self.d )
		self.prevError=error

		u=self.kp*error + self.ki*self.i + self.kd*self.d

		self.clamped=ADV_Controller.clamp(self,u)
		return u

#Integrator.py
import feedback
import feedback.Component



'''
計算輸入的積分,需要將累積項乘以每個時間幀的間隔
	data 存放所有arr的和
'''
class Integrator(feedback.Component.Component):
	def __init__(self):self.data=0
	def work(self,arr):
		self.data+=arr
		return feedback.deltaT*self.data

#FixedFilter.py
import feedback.Component



'''
平滑過濾器FixedFilter計算最近n次輸入的未加權的平均值

	n    輸入的未加權的數的個數
	data 存放所有arr的和
'''
class FixedFilter(feedback.Component.Component):
	def __init__(self,n):
		self.n=n
		self.data=[]

	def work(self,arr):
		self.data.append(arr)
		if len(self.data)>self.n:
			self.data.pop(0)  #移除列表data的第一個元素
		return float(sum(self.data))/len(self.data)
#RecurFilter.py
import feedback.Component


'''
平滑過濾器RecurFilter用簡單的指數平滑算法實現
S(t)=α * X(t) + (1 - α) * S(t-1)

	ratio  當前值佔輸出值的比例
	y      輸出值
'''
class RecurFilter(feedback.Component.Component):
	def __init__(self,ratio):
		self.ratio=ratio
		self.y=0
	#混合當前值與原始值
	def work(self,arr):
		self.y=self.ratio * arr +(1- self.ratio)*self.y
		return self.y


#Plant.py
import feedback.Component

'''
工廠類,也是組件之一。在模擬中作爲系統的基類存在!

'''
class Plant(feedback.Component.Component):
	pass

#Loop.py
import feedback
import feedback.Component


'''
    GetTarget	傳遞的目標值函數
	controller  傳遞的控制器,例如PID_Controller的實例
	plant       傳遞的模擬系統
	tm			系統經歷的時間,以deltaT爲單位(默認爲秒)。
	inverted    是否反轉error的公式。反轉爲error=y-r,本來error=r-y。
	trainer     傳遞的執行器。執行器放在控制器和系統之間!
	filter   	傳遞的過濾器。過濾器放在反饋迴路中,即把系統輸出值y過濾後得到y2,用這個y2來算error!
	
'''


def ClosedLoop(GetTarget,controller,plant,
			   tm=500,inverted=False,
				trainer=feedback.Component.Component(),
			   filter=feedback.Component.Component()):

	print("==============模擬開始==================")

    #系統輸出值經過濾器後的值
	y2=0
	for t in range(tm):
		r=GetTarget(t)
		error=r-y2
		if inverted : error = - error
		#得到控制器的輸出值u
		u=controller.work(error)
		#得到執行器輸出值v,即系統實際輸入值
		v=trainer.work(u)
		#得到系統輸出值y
		y=plant.work(v)
		#記錄輸出值
		feedback.yForPlot.append(y)
		#過濾輸出值
		y2=filter.work(y)

		#打印看看!!!
		#print("遍歷:",t,",時間:",t*feedback.deltaT,",目標:",r,",輸出:",y,"\n",
		#	  "中間變量:u=",u,",v=",v,",y2=",y2,"[",plant.monitoring(),"]\n")

	print("==============模擬結束==================")







'''
這個函數內設置我們需要的目標值,交給用戶自己設置
'''
def GetTarget(arr):
	pass



'''
	plant	模擬系統
	traversal	輸入的遍歷變量,是一個列表
	tm	遍歷最大值
'''
def OpenedLoop(plant,traversal,tm=500):
	print("==============模擬開始==================")
	for t in range(tm):
		#得到系統輸出值
		y=plant.work(traversal[t])
		#記錄輸出值
		feedback.yForPlot.append(y)
	print("==============模擬結束==================")


# #測試樣式
# p=Plant()
# c=PID_Controller(0.5,0.05)
# ClosedLoop(GetTarget,c,p)



案例:高速緩存命中率

這個案例會介紹一種經典的反饋原理應用,它使用調整高速緩存容量的方法來改進高速緩存的命中率

受控系統是一個高速緩存器,例如Web緩存器或者數據庫緩存器。假設高速緩存器容量爲n,使用“最近訪問協議”的高速緩存器。

“最近訪問協議”
如果用戶請求的條目已經在高速緩存器中找到,就將其返回給請求者,如果沒有找到就從後臺存儲器中獲取,然後返回給請求者,同時將此內容添加到高速緩存器中。如果高速緩存器中的條目超過n,那麼就把最老的條目刪除掉。

要求就是70%的請求應從高速緩存器中獲取,並且高速緩存器應儘可能小,以便最大程度地減少內存消耗,併爲其他任務騰出空間!

定義組件

在這裏插入圖片描述
請求”設在某個概率上浮動的數A,想訪問的條目在某個概率上的數B,因爲實際系統千差萬別,這裏可以隨便設個A與B的關係,比如沒有關係!

緩存的設計就簡單了,只需要用一個字典表示高速緩存器就行,後臺緩存器可以省略。因爲後臺緩存器的使用是在沒命中的情況下,去後臺緩存中查找條目,再返回給用戶,並且保留一份在高速緩存中——在這個過程中,後臺查找的步驟可以省略,返回給用戶也可以省略,我們只需要命中率就行!所以後臺緩存器也就可以省略了。

因爲這裏討論的只是如何用反饋思維解決控制的問題,不考慮性能,所以能省就省。

系統輸入爲緩存大小,輸出爲命中率!這樣就可以通過緩存大小來影響命中率了!!!

如何判斷條目的新舊
用遍歷的變量就可以,當某個條目被訪問到,即在字典中被查找到後,就更新該條目的值爲變量的變量。
當條目被訪問到就返回1,未被訪問到就返回0,通過取多次的平均值,記爲該次訪問的命中率。

#main.py
import feedback as fb
import feedback.Plant
import feedback.FixedFilter
import feedback.RecurFilter
import feedback.PID_Controller
import feedback.ADV_Controller
import feedback.Loop
import random
import numpy as np
import matplotlib.pyplot as plt
from  mpl_toolkits.axisartist.axislines import SubplotZero








'''
緩存器
這個系統傳入u,傳出0/1,經過過濾後會得到一個小數,這個小數就是命中率


	t 			遍歷變量,可作爲時間。
				可以當做條目新舊的判據,當訪問到某條目時就更新其數據爲t
	size		條目數量
	cache		緩存。這個字典的鍵是條目,值是遍歷變量t
	SendDemand	傳遞的要求函數
	
'''

#記錄緩存器大小的變量,用於作圖
yCacheSize=[]


class Cache(fb.Plant.Plant):
	def __init__(self,size,SendDemand):
		self.t=0
		self.size=size
		self.cache={}
		self.SendDemand=SendDemand

	def work(self,u):
		self.t+=1
		#size爲非零整數
		self.size = max(0,int(u))

		#記錄緩存器大小
		yCacheSize.append(self.size)


		#取出對應的條目,這裏是個小的不確定的模擬系統,就用一個概率代替了
		#item是個隨機數,模擬請求到的條目/數據
		item = self.SendDemand(self.t)


		if  item in self.cache:
			self.cache[item]=self.t  #更新條目,表示此條目最近訪問過
			return 1


		#刪除舊的條目
		cacheLen=len(self.cache)
		if cacheLen>= self.size:
			#要被刪除的數量
			num = cacheLen - self.size + 1
			tmp={}
			for k in self.cache.keys():
				tmp[self.cache[k]]=k
			for t in sorted(tmp.keys()):
				#刪除最老的元素
				del self.cache[tmp[t]]
				num -= 1
				if num == 0:break

		#不在緩存中的條目添加進緩存,這裏忽略了從後臺緩存中查找。
		self.cache[item]=self.t
		return 0







'''
高速緩存器

	n 		使用FixedFilter過濾器時,平均計算間隔
	ratio	使用RecurFilter過濾器時,當前傳入數佔之後數的比例
			因爲要對傳出的0/1進行求和平均,所以只能使用FixedFilter這個過濾器
	f		過濾器
'''
class SmoothCache(Cache):
	def __init__(self,size,demand,n):
		super().__init__(size,demand)
		self.f = fb.FixedFilter.FixedFilter(n)
	# def __init__(self, size, demand, ratio):
	# 	super().__init__(size, demand)
	# 	self.f = fb.RecurFilter.RecurFilter(ratio)
	#arr是輸入值,先經過一個緩存系統,得到的輸出值再經過一個過濾器輸出
	def work(self,arr):
		#print("系統輸入>",arr,end="")
		y=super().work(arr)
		yy=self.f.work(y)
		#print("系統輸出>",yy)
		return yy
	def monitoring(self):
		return "高速緩存器"




'''
目標函數,返回需要的曲線。這裏是命中率

目標曲線如下
t=[0,  2500,  2500, 4500, 4500, 7800, 7800, 10000]
y=[0.7,0.7,  0.9,0 .9, 0.15,0.15 ,0.5, 0.5]
'''
def GetTarget(arr):
	if arr<2500:return 0.7
	if arr<4500:return 0.9
	if arr<7800:return 0.15
	return 0.5




'''
需求函數
要是分三個不同的概率那就特別不準了。。。

高斯分佈:以 mu 爲均值,sigma 爲標準差
random.gauss(mu, sigma)        
'''
def SendDemand(arr):
	# if arr<3000:return int(random.gauss(0,15))
	# elif arr<5000:return int(random.gauss(0,35))
	# else:return int(random.gauss(100,15))
	return int(random.gauss(0,15))



'''
工具作圖
'''
def showInPlot():
	# 實際的輸出數量的曲線
	xS = np.arange(0, 10000, 1)
	yS = fb.yForPlot

	# 目標曲線
	xTarget = [0, 2500, 2500, 4500, 4500, 7800, 7800, 10000]
	yTarget = [0.7, 0.7, 0.9, 0.9, 0.15, 0.15, 0.5, 0.5]

	#第一個子圖
	plt.subplot(2,1,1)
	#實際命中率(系統輸出)
	plt.plot(xS, yS, color="g", linestyle="-", linewidth=1.0, label="real rate")
	#目標命中率
	plt.plot(xTarget, yTarget, color="b", linestyle="-", linewidth=1.0, label="targeted rate")
	plt.xlabel("traversal variable is t")
	plt.ylabel("cache hit rate")
	plt.legend(loc="upper right", bbox_to_anchor=(0.9, 0.95))


	#第二個子圖
	plt.subplot(2, 1,2)
	#緩存器大小(系統輸入)
	plt.plot(xS, yCacheSize, color="m", linestyle="-", linewidth=1.0, label="cache size")
	plt.xlabel("traversal variable is t")
	plt.ylabel("cache size")
	plt.legend(loc="upper right", bbox_to_anchor=(0.9, 0.95))
	plt.show()




if __name__=="__main__":
	#
	# #傳遞條目數量爲零、需求函數、過濾器的間隔或之前值佔之後值比例
	p=SmoothCache(0,SendDemand,150) #每10個平均計算一次命中率

	#傳遞比例項係數、積分項係數、微分項係數
	#c=fb.PID_Controller.PID_Controller(1.5,1.3)
	c=fb.ADV_Controller.ADV_Controller(1.5,1.0,1.01,ratio=0.9)


	fb.Loop.ClosedLoop(GetTarget,c,p,10000)


	#作圖
	showInPlot()


	# print(fb.__name__,fb.__author__,fb.__all__,fb.deltaT)
	# if 'deltaT' in globals().keys():	print('yes')
	# else:print('no')
	#
	# print(globals().keys())

具有控制器的系統輸出和控制系統輸出的效果
在這裏插入圖片描述

沒有控制器的系統輸出的效果

	#不進行控制,顯示本來的面貌
	p = SmoothCache(0, SendDemand, 150)
	t=np.arange(0, 200, 0.02)
	fb.Loop.OpenedLoop(p,t,10000)

在這裏插入圖片描述

高速緩存器越大,命中率越大,但是容量超過100後命中率基本沒有變化。一百以內容量與命中率的關係是線性的。

挺奇怪的結論!100這個數也太小了,實際容量都遠遠高於這個數,等於就是說高速緩存器容量和命中率沒什麼關係(誤)。

其實原因是請求太少了,請求的種類多少影響緩存器的大小。

#重寫SendDemand
return int(random.gauss(300,115))
#更改遍歷的參數
t=np.arange(0, 900, 0.09)

在增加了請求種類後,相同的緩存大小下命中率就下降了。
在這裏插入圖片描述

結論

當請求種類大小不變時,增加緩存器大小可以增加命中率。但是超過某個臨界點,命中率就與緩存器大小無關了。


參考:《企業級編程與控制理論》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章