comparison mercurial/util.py @ 39567:ee087f0d7db5

util: allow lrucachedict to track cost of entries Currently, lrucachedict allows tracking of arbitrary items with the only limit being the total number of items in the cache. Caches can be a lot more useful when they are bound by the size of the items in them rather than the number of elements in the cache. In preparation for teaching lrucachedict to enforce a max size of cached items, we teach lrucachedict to optionally associate a numeric cost value with each node. We purposefully let the caller define their own cost for nodes. This does introduce some overhead. Most of it comes from __setitem__, since that function now calls into insert(), thus introducing Python function call overhead. $ hg perflrucachedict --size 4 --gets 1000000 --sets 1000000 --mixed 1000000 ! gets ! wall 0.599552 comb 0.600000 user 0.600000 sys 0.000000 (best of 17) ! wall 0.614643 comb 0.610000 user 0.610000 sys 0.000000 (best of 17) ! inserts ! <not available> ! wall 0.655817 comb 0.650000 user 0.650000 sys 0.000000 (best of 16) ! sets ! wall 0.540448 comb 0.540000 user 0.540000 sys 0.000000 (best of 18) ! wall 0.805644 comb 0.810000 user 0.810000 sys 0.000000 (best of 13) ! mixed ! wall 0.651556 comb 0.660000 user 0.660000 sys 0.000000 (best of 15) ! wall 0.781357 comb 0.780000 user 0.780000 sys 0.000000 (best of 13) $ hg perflrucachedict --size 1000 --gets 1000000 --sets 1000000 --mixed 1000000 ! gets ! wall 0.621014 comb 0.620000 user 0.620000 sys 0.000000 (best of 16) ! wall 0.615146 comb 0.620000 user 0.620000 sys 0.000000 (best of 17) ! inserts ! <not available> ! wall 0.698115 comb 0.700000 user 0.700000 sys 0.000000 (best of 15) ! sets ! wall 0.560247 comb 0.560000 user 0.560000 sys 0.000000 (best of 18) ! wall 0.832495 comb 0.830000 user 0.830000 sys 0.000000 (best of 12) ! mixed ! wall 0.686172 comb 0.680000 user 0.680000 sys 0.000000 (best of 15) ! wall 0.841359 comb 0.840000 user 0.840000 sys 0.000000 (best of 12) We're still under 1us per insert, which seems like reasonable performance for a cache. If we comment out updating of self.totalcost during insert(), performance of insert() is identical to __setitem__ before. However, I don't want to make total cost evaluation lazy because it has significant performance implications for when we need to evaluate the total cost at mutation time (it requires a cache traversal, which could be expensive for large caches). Differential Revision: https://phab.mercurial-scm.org/D4502
author Gregory Szorc <gregory.szorc@gmail.com>
date Fri, 07 Sep 2018 12:14:42 -0700
parents bd9d3a89f07b
children 842cd0bdda75
comparison
equal deleted inserted replaced
39566:bd9d3a89f07b 39567:ee087f0d7db5
1207 """A node in a doubly linked list. 1207 """A node in a doubly linked list.
1208 1208
1209 Holds a reference to nodes on either side as well as a key-value 1209 Holds a reference to nodes on either side as well as a key-value
1210 pair for the dictionary entry. 1210 pair for the dictionary entry.
1211 """ 1211 """
1212 __slots__ = (u'next', u'prev', u'key', u'value') 1212 __slots__ = (u'next', u'prev', u'key', u'value', u'cost')
1213 1213
1214 def __init__(self): 1214 def __init__(self):
1215 self.next = None 1215 self.next = None
1216 self.prev = None 1216 self.prev = None
1217 1217
1218 self.key = _notset 1218 self.key = _notset
1219 self.value = None 1219 self.value = None
1220 self.cost = 0
1220 1221
1221 def markempty(self): 1222 def markempty(self):
1222 """Mark the node as emptied.""" 1223 """Mark the node as emptied."""
1223 self.key = _notset 1224 self.key = _notset
1225 self.value = None
1226 self.cost = 0
1224 1227
1225 class lrucachedict(object): 1228 class lrucachedict(object):
1226 """Dict that caches most recent accesses and sets. 1229 """Dict that caches most recent accesses and sets.
1227 1230
1228 The dict consists of an actual backing dict - indexed by original 1231 The dict consists of an actual backing dict - indexed by original
1231 1234
1232 The head node is the newest entry in the cache. If the cache is full, 1235 The head node is the newest entry in the cache. If the cache is full,
1233 we recycle head.prev and make it the new head. Cache accesses result in 1236 we recycle head.prev and make it the new head. Cache accesses result in
1234 the node being moved to before the existing head and being marked as the 1237 the node being moved to before the existing head and being marked as the
1235 new head node. 1238 new head node.
1239
1240 Items in the cache can be inserted with an optional "cost" value. This is
1241 simply an integer that is specified by the caller. The cache can be queried
1242 for the total cost of all items presently in the cache.
1236 """ 1243 """
1237 def __init__(self, max): 1244 def __init__(self, max):
1238 self._cache = {} 1245 self._cache = {}
1239 1246
1240 self._head = head = _lrucachenode() 1247 self._head = head = _lrucachenode()
1241 head.prev = head 1248 head.prev = head
1242 head.next = head 1249 head.next = head
1243 self._size = 1 1250 self._size = 1
1244 self.capacity = max 1251 self.capacity = max
1252 self.totalcost = 0
1245 1253
1246 def __len__(self): 1254 def __len__(self):
1247 return len(self._cache) 1255 return len(self._cache)
1248 1256
1249 def __contains__(self, k): 1257 def __contains__(self, k):
1259 def __getitem__(self, k): 1267 def __getitem__(self, k):
1260 node = self._cache[k] 1268 node = self._cache[k]
1261 self._movetohead(node) 1269 self._movetohead(node)
1262 return node.value 1270 return node.value
1263 1271
1264 def __setitem__(self, k, v): 1272 def insert(self, k, v, cost=0):
1273 """Insert a new item in the cache with optional cost value."""
1265 node = self._cache.get(k) 1274 node = self._cache.get(k)
1266 # Replace existing value and mark as newest. 1275 # Replace existing value and mark as newest.
1267 if node is not None: 1276 if node is not None:
1277 self.totalcost -= node.cost
1268 node.value = v 1278 node.value = v
1279 node.cost = cost
1280 self.totalcost += cost
1269 self._movetohead(node) 1281 self._movetohead(node)
1270 return 1282 return
1271 1283
1272 if self._size < self.capacity: 1284 if self._size < self.capacity:
1273 node = self._addcapacity() 1285 node = self._addcapacity()
1275 # Grab the last/oldest item. 1287 # Grab the last/oldest item.
1276 node = self._head.prev 1288 node = self._head.prev
1277 1289
1278 # At capacity. Kill the old entry. 1290 # At capacity. Kill the old entry.
1279 if node.key is not _notset: 1291 if node.key is not _notset:
1292 self.totalcost -= node.cost
1280 del self._cache[node.key] 1293 del self._cache[node.key]
1281 1294
1282 node.key = k 1295 node.key = k
1283 node.value = v 1296 node.value = v
1297 node.cost = cost
1298 self.totalcost += cost
1284 self._cache[k] = node 1299 self._cache[k] = node
1285 # And mark it as newest entry. No need to adjust order since it 1300 # And mark it as newest entry. No need to adjust order since it
1286 # is already self._head.prev. 1301 # is already self._head.prev.
1287 self._head = node 1302 self._head = node
1288 1303
1304 def __setitem__(self, k, v):
1305 self.insert(k, v)
1306
1289 def __delitem__(self, k): 1307 def __delitem__(self, k):
1290 node = self._cache.pop(k) 1308 node = self._cache.pop(k)
1309 self.totalcost -= node.cost
1291 node.markempty() 1310 node.markempty()
1292 1311
1293 # Temporarily mark as newest item before re-adjusting head to make 1312 # Temporarily mark as newest item before re-adjusting head to make
1294 # this node the oldest item. 1313 # this node the oldest item.
1295 self._movetohead(node) 1314 self._movetohead(node)
1304 return default 1323 return default
1305 1324
1306 def clear(self): 1325 def clear(self):
1307 n = self._head 1326 n = self._head
1308 while n.key is not _notset: 1327 while n.key is not _notset:
1328 self.totalcost -= n.cost
1309 n.markempty() 1329 n.markempty()
1310 n = n.next 1330 n = n.next
1311 1331
1312 self._cache.clear() 1332 self._cache.clear()
1313 1333
1334 n = n.prev 1354 n = n.prev
1335 1355
1336 # We could potentially skip the first N items when decreasing capacity. 1356 # We could potentially skip the first N items when decreasing capacity.
1337 # But let's keep it simple unless it is a performance problem. 1357 # But let's keep it simple unless it is a performance problem.
1338 for i in range(len(self._cache)): 1358 for i in range(len(self._cache)):
1339 result[n.key] = n.value 1359 result.insert(n.key, n.value, cost=n.cost)
1340 n = n.prev 1360 n = n.prev
1341 1361
1342 return result 1362 return result
1343 1363
1344 def popoldest(self): 1364 def popoldest(self):
1357 1377
1358 key, value = n.key, n.value 1378 key, value = n.key, n.value
1359 1379
1360 # And remove it from the cache and mark it as empty. 1380 # And remove it from the cache and mark it as empty.
1361 del self._cache[n.key] 1381 del self._cache[n.key]
1382 self.totalcost -= n.cost
1362 n.markempty() 1383 n.markempty()
1363 1384
1364 return key, value 1385 return key, value
1365 1386
1366 def _movetohead(self, node): 1387 def _movetohead(self, node):