File: lrucacheExtension.pyx

package info (click to toggle)
pytables 2.3.1-3
  • links: PTS, VCS
  • area: main
  • in suites: wheezy
  • size: 14,624 kB
  • sloc: python: 55,641; xml: 15,737; ansic: 5,805; sh: 460; makefile: 234
file content (642 lines) | stat: -rw-r--r-- 20,765 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
#  Ei!, emacs, this is -*-Python-*- mode
########################################################################
#
#       License: BSD
#       Created: Aug 13, 2006
#       Author:  Francesc Alted - faltet@pytables.com
#
#       $Id: indexesExtension.pyx 1756 2006-08-13 10:19:29Z faltet $
#
########################################################################

"""Pyrex interface for several LRU cache systems.

Classes (type extensions):

    NodeCache
    ObjectCache
    NumCache

Functions:

Misc variables:

    __version__
"""

import sys

import numpy
from definitions cimport \
  memcpy, strcmp, \
  import_array, ndarray

from tables.parameters import \
  DISABLE_EVERY_CYCLES, ENABLE_EVERY_CYCLES, LOWEST_HIT_RATIO



#----------------------------------------------------------------------------
# Initialization code.
# The numpy API requires this function to be called before
# using any numpy facilities in an extension module.
import_array()
#----------------------------------------------------------------------------



# ------- Minimalist NodeCache for nodes in PyTables ---------

# The next NodeCache code relies on the fact that a node that is
# fetched from the cache will be removed from it. Said in other words:
# "A node cannot be alive and dead at the same time."

# Thanks to the above behaviour, the next code has been stripped down
# to a bare minimum (the info in cache is kept in just 2 lists).

#*********************** Important note! *****************************
# The code behind has been carefully tuned to serve the needs of
# PyTables cache for nodes. As a consequence, it is no longer
# appropriate as a general LRU cache implementation. You have been
# warned!.  F. Alted 2006-08-08
#*********************************************************************


cdef class NodeCache:
  """Least-Recently-Used (LRU) cache for PyTables nodes."""
  # This class variables are declared in utilsExtension.pxd


  def __init__(self, nslots):
    """Maximum nslots of the cache.
    If more than 'nslots' elements are added to the cache,
    the least-recently-used ones will be discarded."""

    if nslots < 0:
      raise ValueError, "Negative number (%s) of slots!" % nslots
    self.nslots = nslots;  self.nextslot = 0
    self.nodes = [];  self.paths = []


  def __len__(self):
    return len(self.nodes)


  def __setitem__(self, path, node):
    self.setitem(path, node)


  cdef setitem(self, object path, object node):
    """Puts a new node in the node list."""

    if self.nslots == 0:   # Oops, the cache is set to empty
      return
    # Check if we are growing out of space
    if self.nextslot == self.nslots:
      # It is critical to reduce nextslot *before* the preemption of
      # the LRU node.  If not, this can lead with problems in situations
      # with very small caches (length 1 or so).
      # F. Alted 2008-10-22
      self.nextslot = self.nextslot - 1
      # Remove the LRU node and path (the start of the lists)
      del self.nodes[0];  del self.paths[0]
    # The equality protection has been put for situations in which a
    # node is being preempted and added simultaneously (with very small
    # caches).
    if len(self.nodes) == len(self.paths):
      # Add the node and path to the end of its lists
      self.nodes.append(node);  self.paths.append(path)
      self.nextslot = self.nextslot + 1


  def __contains__(self, path):
    if self.getslot(path) == -1:
      return 0
    else:
      return 1


  cdef long getslot(self, object path):
    """Checks whether path is in this cache or not."""
    cdef long i, nslot

    nslot = -1  # -1 means not found
    # Start looking from the trailing values (most recently used)
    for i from self.nextslot > i >= 0:
      if strcmp(path, self.paths[i]) == 0:
        nslot = i
        break
    return nslot


  def pop(self, path):
    return self.cpop(path)


  cdef object cpop(self, object path):
    cdef long nslot

    nslot = self.getslot(path)
    node = self.nodes[nslot]
    del self.nodes[nslot];  del self.paths[nslot]
    self.nextslot = self.nextslot - 1
    return node


  def __iter__(self):
    # Do a copy of the paths list because it can be modified in the middle of
    # the iterator!
    copy = self.paths[:]
    return iter(copy)


  def __repr__(self):
    return "<%s (%d elements)>" % (str(self.__class__), len(self.paths))



########################################################################
# Common code for other LRU cache classes
########################################################################

cdef class BaseCache:
  """Base class that implements automatic probing/disabling of the cache.
  """

  def __init__(self, long nslots, object name):

    if nslots < 0:
      raise ValueError, "Negative number (%s) of slots!" % nslots
    self.setcount = 0;  self.getcount = 0;  self.containscount = 0
    self.enablecyclecount = 0;  self.disablecyclecount = 0
    self.iscachedisabled = False  # Cache is enabled by default
    self.disableeverycycles = DISABLE_EVERY_CYCLES
    self.enableeverycycles = ENABLE_EVERY_CYCLES
    self.lowesthr = LOWEST_HIT_RATIO
    self.nprobes = 0.0;  self.hitratio = 0.0
    self.nslots = nslots
    self.seqn_ = 0;  self.nextslot = 0
    self.name = name
    self.incsetcount = False
    # The array for keeping the access times (using long ints here)
    self.atimes = <ndarray>numpy.zeros(shape=nslots, dtype=numpy.int_)
    self.ratimes = <long *>self.atimes.data


  def __len__(self):
    return self.nslots


  # Machinery for determining whether the hit ratio is being effective
  # or not.  If not, the cache will be disabled. The efficency will be
  # checked every cycle (the time that the cache would be refilled
  # completely).  In situations where the cache is not being re-filled
  # (i.e. it is not enabled) for a long time, it is forced to be
  # re-enabled when a certain number of cycles has passed so as to
  # check whether a new scenario where the cache can be useful again
  # has come.
  # F. Alted 2006-08-09
  cdef int checkhitratio(self):
    cdef double hitratio
    cdef long nslot

    if self.setcount > self.nslots:
      self.disablecyclecount = self.disablecyclecount + 1
      self.enablecyclecount = self.enablecyclecount + 1
      self.nprobes = self.nprobes + 1
      hitratio = <double>self.getcount / self.containscount
      self.hitratio = self.hitratio + hitratio
      # Reset the hit counters
      self.setcount = 0;  self.getcount = 0;  self.containscount = 0
      if (not self.iscachedisabled and
          self.disablecyclecount >= self.disableeverycycles):
        # Check whether the cache is being effective or not
        if hitratio < self.lowesthr:
          # Hit ratio is low. Disable the cache.
          self.iscachedisabled = True
        else:
          # Hit ratio is acceptable. (Re-)Enable the cache.
          self.iscachedisabled = False
        self.disablecyclecount = 0
      if self.enablecyclecount >= self.enableeverycycles:
        # We have reached the time for forcing the cache to act again
        self.iscachedisabled = False
        self.enablecyclecount = 0
    return not self.iscachedisabled


  def couldenablecache(self):
    return self.couldenablecache_()


  # Check whether the cache is enabled or *could* be enabled in the next
  # setitem operation. This method can be used in order to probe whether
  # an (expensive) operation to be done before a .setitem() is worth the
  # effort or not.
  cdef int couldenablecache_(self):

    if self.nslots == 0:
      return False
    # Increment setitem because it can be that .setitem() doesn't
    # get called after calling this.
    self.setcount = self.setcount + 1;  self.incsetcount = True
    if self.iscachedisabled:
      if self.setcount == self.nslots:
        # The cache *could* be enabled in the next setitem operation
        return True
      else:
        return False
    else:
      return True


  # Increase the access time (implemented as a C long sequence)
  cdef long incseqn(self):

    self.seqn_ = self.seqn_ + 1
    if self.seqn_ < 0:
      # Ooops, the counter has run out of range! Reset all the access times.
      self.atimes[:] = sys.maxint
      # Set the counter to 1 (to indicate that it is newer than existing ones)
      self.seqn_ = 1
    return self.seqn_


  def __repr__(self):
    return "<%s(%s) (%d elements)>" % \
           (self.name, str(self.__class__), self.nslots)



########################################################################
#  Helper class for ObjectCache
########################################################################

cdef class ObjectNode:
  """Record of a cached value. Not for public consumption."""

  def __init__(self, object key, object obj, long nslot):
    object.__init__(self)
    self.key = key
    self.obj = obj
    self.nslot = nslot


  def __repr__(self):
    return "<%s %s (slot #%s) => %s>" % \
           (self.__class__, self.key, self.nslot, self.object)


########################################################################
#  Minimalistic LRU cache implementation for general python objects
#        This is a *true* general lru cache for python objects
########################################################################

cdef class ObjectCache(BaseCache):
  """Least-Recently-Used (LRU) cache specific for python objects.
  """

  def __init__(self, long nslots, long maxcachesize, object name):
    """Maximum size of the cache.
    If more than 'nslots' elements are added to the cache,
    the least-recently-used ones will be discarded.

    Parameters:
    nslots - The number of slots in cache
    name - A descriptive name for this cache
    """

    super(ObjectCache, self).__init__(nslots, name)
    self.cachesize = 0
    self.maxcachesize = maxcachesize
    # maxobjsize will be the same as the maximum cache size
    self.maxobjsize = maxcachesize
    self.__list = [None]*nslots
    self.__dict = {}
    self.mrunode = <ObjectNode>None   # Most Recent Used node
    # The array for keeping the object size (using long ints here)
    self.sizes = <ndarray>numpy.zeros(shape=nslots, dtype=numpy.int_)
    self.rsizes = <long *>self.sizes.data


  # Clear cache
  cdef clearcache_(self):
    self.__list = [None]*self.nslots
    self.__dict = {}
    self.mrunode = <ObjectNode>None
    self.cachesize = 0
    self.nextslot = 0
    self.seqn_ = 0


  # Remove a slot (if it exists in cache)
  cdef removeslot_(self, long nslot):
    cdef ObjectNode node

    assert nslot < self.nslots, "Attempting to remove beyond cache capacity."
    node = self.__list[nslot]
    if node is not None:
      self.__list[nslot] = None
      del self.__dict[node.key]
      self.cachesize = self.cachesize - self.rsizes[nslot]
      self.rsizes[nslot] = 0
      if self.mrunode and self.mrunode.nslot == nslot:
        self.mrunode = <ObjectNode>None
    # The next slot to be updated will be this one
    self.nextslot = nslot


  # Update a slot
  cdef updateslot_(self, long nslot, long size, object key, object value):
    cdef ObjectNode node, oldnode
    cdef long nslot1, nslot2
    cdef object lruidx

    assert nslot < self.nslots, "Number of nodes exceeding cache capacity."
    # Remove the previous nslot
    self.removeslot_(nslot)
    # Protection against too large data cache size
    while size + self.cachesize > self.maxcachesize:
      # Remove the LRU node among the 10 largest ones
      largidx = self.sizes.argsort()[-10:]
      nslot1 = self.atimes[largidx].argmin()
      nslot2 = largidx[nslot1]
      self.removeslot_(nslot2)
    # Insert the new one
    node = ObjectNode(key, value, nslot)
    self.ratimes[nslot] = self.incseqn()
    self.rsizes[nslot] = size
    self.__list[nslot] = node
    self.__dict[key] = node
    self.mrunode = node
    self.cachesize = self.cachesize + size
    # The next slot to update will be the LRU
    self.nextslot = self.atimes.argmin()


  # Put the object to the data in cache (for Python calls)
  def setitem(self, object key, object value, object size):
    return self.setitem_(key, value, size)


  # Put the object in cache (for Pyrex calls)
  # size can be the exact size of the value object or an estimation.
  cdef long setitem_(self, object key, object value, long size):
    cdef long nslot

    if self.nslots == 0:   # The cache has been set to empty
      return -1
    nslot = -1
    # Perhaps setcount has been already incremented in couldenablecache()
    if not self.incsetcount:
      self.setcount = self.setcount + 1
    else:
      self.incsetcount = False
    if size > self.maxobjsize:  # Check if the object is too large
      return -1
    if self.checkhitratio():
      nslot = self.nextslot
      self.updateslot_(nslot, size, key, value)
    else:
      # Empty the cache because it is not effective and it is taking space
      self.clearcache_()
    return nslot


  # Tells whether the key is in cache or not
  def __contains__(self, object key):
    return self.__dict.has_key(key)


  # Tells in which slot the key is. If not found, -1 is returned.
  def getslot(self, object key):
    return self.getslot_(key)


  # Tells in which slot the key is. If not found, -1 is returned.
  cdef long getslot_(self, object key):
    cdef ObjectNode node

    if self.nslots == 0:   # The cache has been set to empty
      return -1
    self.containscount = self.containscount + 1
    # Give a chance to the MRU node
    node = self.mrunode
    if node and node.key == key:
      return node.nslot
    # No luck. Look in the dictionary.
    node = self.__dict.get(key)
    if node is <ObjectNode>None:
      return -1
    return node.nslot

  # Return the object to the data in cache (for Python calls)
  def getitem(self, object nslot):
    return self.getitem_(nslot)


  # Return the object to the data in cache (for Pyrex calls)
  cdef object getitem_(self, long nslot):
    cdef ObjectNode node

    self.getcount = self.getcount + 1
    node = self.__list[nslot]
    self.ratimes[nslot] = self.incseqn()
    self.mrunode = node
    return node.obj


  def __repr__(self):
    if self.nprobes > 0:
      hitratio = self.hitratio / self.nprobes
    else:
      hitratio = <double>self.getcount / self.containscount
    return """<%s(%s)
  (%d maxslots, %d slots used, %.3f KB cachesize,
  hit ratio: %.3f, disabled? %s)>
  """ % \
           (self.name, str(self.__class__), self.nslots, self.nextslot,
            self.cachesize / 1024., hitratio, self.iscachedisabled)


###################################################################
#  Minimalistic LRU cache implementation for numerical data
###################################################################
# The next code is more efficient in situations where efficiency is low.
###################################################################

#*********************** Important note! ****************************
# The code behind has been carefully tuned to serve the needs of
# caching numerical data. As a consequence, it is no longer appropriate
# as a general LRU cache implementation. You have been warned!.
# F. Alted 2006-08-09
#********************************************************************

cdef class NumCache(BaseCache):
  """Least-Recently-Used (LRU) cache specific for Numerical data.
  """

  def __init__(self, object shape, object dtype, object name):
    """Maximum size of the cache.
    If more than 'nslots' elements are added to the cache,
    the least-recently-used ones will be discarded.

    Parameters:
    shape - The rectangular shape of the cache (nslots, nelemsperslot)
    itemsize - The size of the element base in cache
    name - A descriptive name for this cache
    """
    cdef long nslots

    nslots = shape[0];  self.slotsize = shape[1]
    if nslots >= 1<<16:
      # nslots can't be higher than 2**16. Will silently trunk the number.
      nslots = <long>((1<<16)-1)  # Cast makes Pyrex happy here
    super(NumCache, self).__init__(nslots, name)
    self.itemsize = dtype.itemsize
    self.__dict = {}
    # The cache object where all data will go
    # The last slot is to allow the setitem1_ method to still return
    # a valid scratch area for writing purposes
    self.cacheobj = <ndarray>numpy.empty(shape=(nslots+1, self.slotsize),
                                         dtype=dtype)
    self.rcache = <void *>self.cacheobj.data
    # The array for keeping the keys of slots
    self.keys = <ndarray>(-numpy.ones(shape=nslots, dtype=numpy.int64))
    self.rkeys = <long long *>self.keys.data


  # Returns the address of nslot
  cdef void *getaddrslot_(self, long nslot):
    if nslot >= 0:
      return <char *>self.rcache + nslot * self.slotsize * self.itemsize
    else:
      return <char *>self.rcache + self.nslots * self.slotsize * self.itemsize


  def setitem(self, long long key, ndarray nparr, long start):
    return self.setitem_(key, nparr.data, start)


  # Copy the new data into a cache slot
  cdef long setitem_(self, long long key, void *data, long start):
    cdef long nslot

    nslot = self.setitem1_(key)
    if nslot >= 0:
      # Copy the data to cache
      memcpy(<char *>self.rcache + nslot * self.slotsize * self.itemsize,
             <char *>data + start * self.itemsize,
             self.slotsize * self.itemsize)
    return nslot


  # Return a cache data pointer appropriate to save data.
  # Even if the cache is disabled, this will return a -1, which is
  # the last element in the cache.
  # This version avoids a memcpy of data, but the user should be
  # aware that data in nslot cannot be overwritten!
  cdef long setitem1_(self, long long key):
    cdef long nslot
    cdef object key2

    if self.nslots == 0:   # Oops, the cache is set to empty
      return -1
    # Perhaps setcount has been already incremented in couldenablecache()
    if not self.incsetcount:
      self.setcount = self.setcount + 1
    else:
      self.incsetcount = False
    nslot = -1
    if self.checkhitratio():
      # Check if we are growing out of space
      if self.nextslot == self.nslots:
        # Get the least recently used slot
        nslot = self.atimes.argmin()
        # Remove the slot from the dict
        key2 = self.keys[nslot]
        del self.__dict[key2]
        self.nextslot = self.nextslot - 1
      else:
        # Get the next slot available
        nslot = self.nextslot
      # Insert the slot in the dictionary
      self.__dict[key] = nslot
      self.keys[nslot] = key
      self.ratimes[nslot] = self.incseqn()
      self.nextslot = self.nextslot + 1
      # The next reduces the performance of the cache in scenarios where
      # the efficicency is near to zero.  I don't understand exactly why.
      # F. Alted 24-03-2008
    elif self.nextslot > 0:
      # Empty the cache if needed
      self.__dict.clear()
      self.nextslot = 0
    return nslot


  def getslot(self, long long key):
    return self.getslot_(key)


  # Tells in which slot key is. If not found, -1 is returned.
  cdef long getslot_(self, long long key):
    cdef object nslot

    self.containscount = self.containscount + 1
    if self.nextslot == 0:   # No chances for finding a slot
      return -1
    try:
      nslot = self.__dict[key]
    except KeyError:
      return -1
    return nslot


  def getitem(self, long nslot, ndarray nparr, long start):
    self.getitem_(nslot, nparr.data, start)


  # This version copies data in cache to data+start.
  # The user should be responsible to provide a large enough data buffer
  # to keep all the data.
  cdef getitem_(self, long nslot, void *data, long start):
    cdef void *cachedata

    cachedata = self.getitem1_(nslot)
    # Copy the data in cache to destination
    memcpy(<char *>data + start * self.itemsize, cachedata,
           self.slotsize * self.itemsize)


  # Return the pointer to the data in cache
  # This version avoids a memcpy of data, but the user should be
  # aware that data in nslot cannot be overwritten!
  cdef void *getitem1_(self, long nslot):

    self.getcount = self.getcount + 1
    self.ratimes[nslot] = self.incseqn()
    return <char *>self.rcache + nslot * self.slotsize * self.itemsize


  def __repr__(self):
    cachesize = (self.nslots * self.slotsize * self.itemsize) / 1024.
    if self.nprobes > 0:
      hitratio = self.hitratio / self.nprobes
    else:
      hitratio = <double>self.getcount / self.containscount
    return """<%s(%s)
  (%d maxslots, %d slots used, %.3f KB cachesize,
  hit ratio: %.3f, disabled? %s)>
  """ % \
  (self.name, str(self.__class__), self.nslots, self.nextslot,
   cachesize, hitratio, self.iscachedisabled)



## Local Variables:
## mode: python
## py-indent-offset: 2
## tab-width: 2
## fill-column: 78
## End: