[Python-Dev] Patch to use dict subclasses in eval(), exec

Jeff Epler jepler@unpythonic.net
Mon, 28 Oct 2002 20:39:55 -0600


--PEIAKu/WMn1b1Hv9
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

Using dict subclasses in eval() and exec
----------------------------------------

With the attached patch, you can now use a dict subclass which implements
__getitem__ as the global or builtin namespace for eval() and exec.

If you do not use the feature, the performance impact is low (<2%).
The fast case adds only one test+branch to each eval_code() call (to
set 'fastglobals' and 'slowbuiltins'), each lookup in globals (to
check 'fastglobals'), and each lookup in builtins (to check 'slowbuiltins').

If you do use the feature, the performance impact is unfortunately
quite substantial (400%).

If you use subclasses of dict with eval()/exec, but do not define __getitem__,
the performance penalty is modest (~10%).

The smaller penalty of using a dict subclass without __getitem__ could
probably be erased if the 'fastglobals' test can check that __getitem__
is not defined

Included are a patch and a test suite.  Note that the patch causes a
failure in test_descr which is testing that __getitem__ is *not* called on
a dictionary subclass.

Pystone timings on a 1GHz Duron system running RedHat Linux 8.0 (gcc
3.2) follow.

    CVS	    +patch   +subclass	+__getattr__
1   2.74    2.76     3          14.05
2   2.74    2.78     3.02       14.07
3   2.74    2.78     3.03       14.07
4   2.76    2.78     3.04       14.08
5   2.76    2.79     3.04       14.1
avg 2.748   2.778    3.026      14.074
%   100     101.1    110.1      512.2

* 2.3a0 CVS (28 Oct 2002)
Pystone(1.1) time for 50000 passes = 2.74
This machine benchmarks at 18248.2 pystones/second
Pystone(1.1) time for 50000 passes = 2.74
This machine benchmarks at 18248.2 pystones/second
Pystone(1.1) time for 50000 passes = 2.74
This machine benchmarks at 18248.2 pystones/second
Pystone(1.1) time for 50000 passes = 2.76
This machine benchmarks at 18115.9 pystones/second
Pystone(1.1) time for 50000 passes = 2.76
This machine benchmarks at 18115.9 pystones/second

* 2.3a0 CVS + patch
Pystone(1.1) time for 50000 passes = 2.78
This machine benchmarks at 17985.6 pystones/second
Pystone(1.1) time for 50000 passes = 2.78
This machine benchmarks at 17985.6 pystones/second
Pystone(1.1) time for 50000 passes = 2.79
This machine benchmarks at 17921.1 pystones/second
Pystone(1.1) time for 50000 passes = 2.79
This machine benchmarks at 17921.1 pystones/second
Pystone(1.1) time for 50000 passes = 2.76
This machine benchmarks at 18115.9 pystones/second

* 2.3a0 CVS + patch + dict subclass w/o __getitem__
(commandline:
./python -c 'class D(dict): pass                          
d = D(globals()); execfile("Lib/test/pystone.py", d)')

Pystone(1.1) time for 50000 passes = 3.03
This machine benchmarks at 16501.7 pystones/second
Pystone(1.1) time for 50000 passes = 3.04
This machine benchmarks at 16447.4 pystones/second
Pystone(1.1) time for 50000 passes = 3
This machine benchmarks at 16666.7 pystones/second
Pystone(1.1) time for 50000 passes = 3.04
This machine benchmarks at 16447.4 pystones/second
Pystone(1.1) time for 50000 passes = 3.02
This machine benchmarks at 16556.3 pystones/second

* 2.3a0 CVS + patch + dict subclass w/__getitem__
(commandline:
./python -c 'class D(dict): __getitem__ = lambda s, i: dict.__getitem__(s, i)
d = D(globals()); execfile("Lib/test/pystone.py", d)')

Pystone(1.1) time for 50000 passes = 14.05
This machine benchmarks at 3558.72 pystones/second
Pystone(1.1) time for 50000 passes = 14.08
This machine benchmarks at 3551.14 pystones/second
Pystone(1.1) time for 50000 passes = 14.07
This machine benchmarks at 3553.66 pystones/second
Pystone(1.1) time for 50000 passes = 14.07
This machine benchmarks at 3553.66 pystones/second
Pystone(1.1) time for 50000 passes = 14.1
This machine benchmarks at 3546.1 pystones/second


--PEIAKu/WMn1b1Hv9
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="ceval-userdict.patch"

Index: Python/ceval.c
===================================================================
RCS file: /cvsroot/python/python/dist/src/Python/ceval.c,v
retrieving revision 2.338
diff -u -r2.338 ceval.c
--- Python/ceval.c	3 Oct 2002 09:53:11 -0000	2.338
+++ Python/ceval.c	29 Oct 2002 00:22:42 -0000
@@ -529,6 +529,8 @@
 	char *filename;
 #endif
 
+	int fastglobals, slowbuiltins;
+
 /* Tuple access macros */
 
 #ifndef Py_DEBUG
@@ -603,6 +605,9 @@
 	names = co->co_names;
 	consts = co->co_consts;
 	fastlocals = f->f_localsplus;
+#define PyDict_CheckExact(d) ((d)->ob_type == &PyDict_Type)
+	fastglobals = PyDict_CheckExact(f->f_globals);
+	slowbuiltins = !PyDict_CheckExact(f->f_builtins);
 	freevars = f->f_localsplus + f->f_nlocals;
 	_PyCode_GETCODEPTR(co, &first_instr);
 	/* An explanation is in order for the next line.
@@ -1596,7 +1601,7 @@
 					     PyObject_REPR(w));
 				break;
 			}
-			err = PyDict_SetItem(x, w, v);
+			err = (fastglobals ? PyDict_SetItem : PyObject_SetItem)(x, w, v);
 			Py_DECREF(v);
 			break;
 
@@ -1675,7 +1680,7 @@
 		case STORE_GLOBAL:
 			w = GETITEM(names, oparg);
 			v = POP();
-			err = PyDict_SetItem(f->f_globals, w, v);
+			err = (fastglobals ? PyDict_SetItem : PyObject_SetItem)(f->f_globals, w, v);
 			Py_DECREF(v);
 			break;
 
@@ -1696,9 +1701,10 @@
 			}
 			x = PyDict_GetItem(x, w);
 			if (x == NULL) {
-				x = PyDict_GetItem(f->f_globals, w);
+				x = (fastglobals ? PyDict_GetItem : PyObject_GetItem)(f->f_globals, w);
 				if (x == NULL) {
-					x = PyDict_GetItem(f->f_builtins, w);
+					if (!fastglobals) PyErr_Clear();
+					x = (slowbuiltins ? PyObject_GetItem : PyDict_GetItem)(f->f_builtins, w);
 					if (x == NULL) {
 						format_exc_check_arg(
 							    PyExc_NameError,
@@ -1711,9 +1717,10 @@
 			PUSH(x);
 			break;
 
+
 		case LOAD_GLOBAL:
 			w = GETITEM(names, oparg);
-			if (PyString_CheckExact(w)) {
+			if (PyString_CheckExact(w) && fastglobals) {
 				/* Inline the PyDict_GetItem() calls.
 				   WARNING: this is an extreme speed hack.
 				   Do not try this at home. */
@@ -1727,6 +1734,7 @@
 						PUSH(x);
 						continue;
 					}
+					if(slowbuiltins) goto load_builtins_slow;
 					d = (PyDictObject *)(f->f_builtins);
 					x = d->ma_lookup(d, w, hash)->me_value;
 					if (x != NULL) {
@@ -1738,9 +1746,11 @@
 				}
 			}
 			/* This is the un-inlined version of the code above */
-			x = PyDict_GetItem(f->f_globals, w);
+			x = PyObject_GetItem(f->f_globals, w);
 			if (x == NULL) {
-				x = PyDict_GetItem(f->f_builtins, w);
+				PyErr_Clear();
+			  load_builtins_slow:
+				x = PyObject_GetItem(f->f_builtins, w);
 				if (x == NULL) {
 				  load_global_error:
 					format_exc_check_arg(

--PEIAKu/WMn1b1Hv9
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="test_dictsubclass.py"

X='xx'

class D(dict):
	def __getitem__(self, item):
		if item is X: return X
		return dict.__getitem__(self, item)

d = D(globals())
d['zz'] = 'zz'

# Make sure the dict acts like it should
print d[X] == X
print d['D'] == D
print d['zz'] == 'zz'
try: d['yy']
except KeyError, detail: print "KeyError", detail
else: print "failed to get exception"

# Make sure that exec and eval() in globals() works right
exec "print (len.__name__, D.__name__)" in globals()
exec "print (lambda: (len.__name__, D.__name__))()" in globals()
print eval("len.__name__, D.__name__", globals())
print eval("(lambda: (len.__name__, D.__name__))()", globals())
try: eval(X, globals())
except NameError, detail: print detail
else: print "failed to get exception"
try: exec "zz" in globals()
except NameError, detail: print detail
else: print "failed to get exception"
try: exec "yy" in globals()
except NameError, detail: print detail
else: print "failed to get exception"

# Make sure that exec and eval() in d works right
exec "print (len.__name__, D.__name__, xx, zz)" in d
exec "print (lambda: (len.__name__, D.__name__, xx, zz))()" in d
print eval("len.__name__, D.__name__, xx, zz", d)
print eval("(lambda: (len.__name__, D.__name__, xx, zz))()", d)
try: exec "yy" in d
except NameError, detail: print detail
else: print "failed to get exception"

--PEIAKu/WMn1b1Hv9
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename=test_dictsubclass

test_dictsubclass
True
True
True
KeyError 'yy'
('len', 'D')
('len', 'D')
('len', 'D')
('len', 'D')
name 'xx' is not defined
name 'zz' is not defined
name 'yy' is not defined
('len', 'D', 'xx', 'zz')
('len', 'D', 'xx', 'zz')
('len', 'D', 'xx', 'zz')
('len', 'D', 'xx', 'zz')
name 'yy' is not defined

--PEIAKu/WMn1b1Hv9--