[Zodb-checkins] CVS: Zope/lib/python/BTrees - BTreeModuleTemplate.c:1.27 BTreeTemplate.c:1.33 Maintainer.txt:1.7

Tim Peters tim.one@comcast.net
Fri, 31 May 2002 16:01:16 -0400


Update of /cvs-repository/Zope/lib/python/BTrees
In directory cvs.zope.org:/tmp/cvs-serv25366

Modified Files:
	BTreeModuleTemplate.c BTreeTemplate.c Maintainer.txt 
Log Message:
Introduced a new BTREE_SEARCH macro for searching interior BTree nodes,
to squash delicate code duplication and for speed.

+ This is optimized in several subtle ways over the current method.  This
  is documented in Maintainer.txt, along with a correctness proof.

+ I'll replace all the BTree searches with this soon.  For now I just
  changed _BTree_get().  I *suspect* this also fixes a subtle bug
  introduced by the recent "don't ignore comparison error" patch:  if a
  comparison did trigger an exception, the function just returned without
  doing the PER_ALLOW_DEACTIVATION + PER_ACCESSED dance.  _BTree_get()
  does that dance again now.


=== Zope/lib/python/BTrees/BTreeModuleTemplate.c 1.26 => 1.27 ===
 #define BTREE(O) ((BTree*)(O))
 
+/* Use BTREE_SEARCH to find which child pointer to follow.
+ * RESULT   An int lvalue to hold the index i such that SELF->data[i].child
+ *          is the correct node to search next.
+ * SELF     A pointer to a BTree node.
+ * KEY      The key you're looking for, of type KEY_TYPE.
+ * ONERROR  What to do if key comparison raises an exception; for example,
+ *          perhaps 'return NULL'.
+ *
+ * See Maintainer.txt for discussion:  this is optimized in subtle ways.
+ * It's recommended that you call this at the start of a routine, waiting
+ * to check for self->len == 0 after.
+ */
+#define BTREE_SEARCH(RESULT, SELF, KEY, ONERROR) {          \
+    int _lo = 0;                                            \
+    int _hi = (SELF)->len;                                  \
+    int _i, _cmp;                                           \
+    for (_i = _hi >> 1; _i > _lo; _i = (_lo + _hi) >> 1) {  \
+        TEST_KEY_SET_OR(_cmp, (SELF)->data[_i].key, (KEY))  \
+            ONERROR;                                        \
+        if      (_cmp < 0) _lo = _i;                        \
+        else if (_cmp > 0) _hi = _i;                        \
+        else   /* equal */ break;                           \
+    }                                                       \
+    (RESULT) = _i;                                          \
+}
+
 typedef struct SetIteration_s
 {
   PyObject *set;


=== Zope/lib/python/BTrees/BTreeTemplate.c 1.32 => 1.33 ===
 _BTree_get(BTree *self, PyObject *keyarg, int has_key)
 {
-  int min, max, i, cmp, copied=1;
-  PyObject *r;
+  int min, copied=1;
+  PyObject *r = NULL;
   KEY_TYPE key;
   
   COPY_KEY_FROM_ARG(key, keyarg, copied);
@@ -30,38 +30,25 @@
 
   PER_USE_OR_RETURN(self, NULL);
 
+  BTREE_SEARCH(min, self, key, goto Error);
   if (self->len)
     {
-      for (min=0, max=self->len, i=max/2; max-min > 1; i=(min+max)/2)
-        {
-          TEST_KEY_SET_OR(cmp, self->data[i].key, key) return NULL;
-          if (cmp < 0) min=i;
-          else if (cmp == 0)
-            {
-              min=i;
-              break;
-            }
-          else max=i;
-        }
-      
       if (SameType_Check(self, self->data[min].child)) 
-        r=_BTree_get( BTREE(self->data[min].child), keyarg, 
-                      has_key ? has_key + 1: 0);
+        r = _BTree_get( BTREE(self->data[min].child), keyarg, 
+                        has_key ? has_key + 1: 0);
       else
-        r=_bucket_get(BUCKET(self->data[min].child), keyarg, 
-                      has_key ? has_key + 1: 0);
+        r = _bucket_get(BUCKET(self->data[min].child), keyarg, 
+                        has_key ? has_key + 1: 0);
     }
   else
     {  /* No data */
       UNLESS (has_key) 
-        {
-          PyErr_SetObject(PyExc_KeyError, keyarg);
-          r=NULL;
-        }
+        PyErr_SetObject(PyExc_KeyError, keyarg);
       else
-        r=PyInt_FromLong(0);
+        r = PyInt_FromLong(0);
     }
 
+Error:
   PER_ALLOW_DEACTIVATION(self);
   PER_ACCESSED(self);
   return r;


=== Zope/lib/python/BTrees/Maintainer.txt 1.6 => 1.7 ===
   an IIBTree has larger values than an OOBTree:  pickles store ints
   more efficiently than they can store arbitrary Python objects.
+
+
+The BTREE_SEARCH Macro
+======================
+For notational ease, consider a fixed BTree node x, and let
+
+    K(i) mean x->data.key[i]
+    C(i) mean all the keys reachable from x->data.child[i]
+
+For each i in 0 to x->len-1 inclusive,
+
+    K(i) <= C(i) < K(i+1)
+
+is a BTree node invariant, where we pretend that K(0) holds a key
+smaller than any possible key, and K(x->len) holds a key larger
+than any possible key.  (Note that K(x->len) doesn't actually exist,
+and K(0) is never used although space for it exists in non-empty
+BTree nodes.)
+
+When searching for a key k, then, the child pointer we want to follow
+is the one at index i such that K(i) <= k < K(i+1).  There can be
+only one such i, since the keys are strictly increasing.  And there is
+at *least* one such i provided the tree isn't empty.  For the moment,
+assume the tree isn't empty (we'll get back to that later).
+
+The macro's chief loop invariant is
+
+    K(lo) < k < K(hi)
+
+This holds trivially at the start, since lo is set to 0 ahd hi to
+x->len, and we pretend K(0) is minus infinity and K(len) is plus
+infinity.  Inside the loop, if K(i) < k we set lo to i, and if
+K(i) > k we set hi to i.  These obviously preserve the invariant.
+If K(i) == k, the loop breaks and sets the result to i, and since
+K(i) == k in that case i is obviously the correct result.
+
+What if the key isn't present?  lo and hi move toward each other,
+narrowing the range, until eventually lo+1 == hi.  At that point,
+i = (lo+hi)/2 = (lo+lo+1)/2 = lo + 1/2 = lo, so that:
+
+1. The loop's "i > lo" test is false, so the loop ends then.
+
+and
+
+2. The invariant still holds, so K(i) < k < K(i+1), and i is again
+   the correct answer.
+
+Can we get out of the loop too early?  No:  if hi = lo + d for some d
+greater than 1, then i = (lo+lo+d)/2 = lo + d/2, and d/2 is at least 1
+since d is at least 2:  i is strictly greater than lo then, and the
+loop continues.
+
+Can lo==hi?  Yes, but only if the node is empty.  Then i, lo and hi
+all start out as 0, and the loop exits immediately.  If the loop
+isn't empty, then lo and hi start out with different values.  Whenever
+lo and hi have different values, lo <= (lo + hi)/2 < hi, so i and lo
+are strictly smaller than hi, so setting either lo or hi to i leaves
+the new lo strictly smaller than the new hi.
+
+Can the loop fail to terminate?  No:  by the above, when lo < hi-1,
+lo < i=(lo+hi)/2 < hi, so setting either lo or hi to i leaves the
+new lo and hi strictly closer to each other than were the old lo and
+hi.
+
+Optimization points:
+
++ Division by 2 is done via shift rather via "/2".  These are
+  signed ints, and almost all C compilers treat signed int division
+  as truncating, and shifting is not the same as truncation for
+  signed int division.  The compiler has no way to know these values
+  aren't negative, so has to generate longer-winded code for "/2".
+  But we know these values aren't negative, and exploit it.
+
++ The order of _cmp comparisons matters.  We're in an interior
+  BTree node, and are looking at only a tiny fraction of all the
+  keys that exist.  So finding the key exactly in this node is
+  unlikely, and checking _cmp == 0 is a waste of time to the same
+  extent.  It doesn't matter whether we check for _cmp < 0 or
+  _cmp > 0 first, so long as we do both before worrying about
+  equality.
+
++ At the start of a routine, it's better to run this macro even
+  if x->len is 0 (check for that afterwards).  We just called a
+  function and so probably drained the pipeline.  If the first thing
+  we do then is read up self->len and check it against 0, we just
+  sit there waiting for the data to get read up, and then another
+  immediate test-and-branch, and for a very unlikely case (BTree
+  nodes are rarely empty).  It's better to get into the loop right
+  away so the normal case makes progress ASAP.