[Python-checkins] r81122 - in python/branches/py3k-jit: Include/code.h JIT JIT/jit_notes.txt Lib/test/support.py Lib/test/test_code.py Lib/test/test_sys.py Objects/codeobject.c Python/ceval.c

jeffrey.yasskin python-checkins at python.org
Wed May 12 18:51:15 CEST 2010


Author: jeffrey.yasskin
Date: Wed May 12 18:51:15 2010
New Revision: 81122

Log:
Import the code hotness model to py3k-jit.  This will help test the background
thread even before we have JITting hooked up.



Added:
   python/branches/py3k-jit/JIT/
   python/branches/py3k-jit/JIT/jit_notes.txt
Modified:
   python/branches/py3k-jit/Include/code.h
   python/branches/py3k-jit/Lib/test/support.py
   python/branches/py3k-jit/Lib/test/test_code.py
   python/branches/py3k-jit/Lib/test/test_sys.py
   python/branches/py3k-jit/Objects/codeobject.c
   python/branches/py3k-jit/Python/ceval.c

Modified: python/branches/py3k-jit/Include/code.h
==============================================================================
--- python/branches/py3k-jit/Include/code.h	(original)
+++ python/branches/py3k-jit/Include/code.h	Wed May 12 18:51:15 2010
@@ -28,6 +28,11 @@
 				   Objects/lnotab_notes.txt for details. */
     void *co_zombieframe;     /* for optimization only (see frameobject.c) */
     PyObject *co_weakreflist;   /* to support weakrefs to code objects */
+#ifdef WITH_LLVM
+    /* Measure of how hot this code object is. This will be used to
+       decide which code objects are worth sending through LLVM. */
+    long co_hotness;
+#endif  /* WITH_LLVM */
 } PyCodeObject;
 
 /* Masks for co_flags above */

Added: python/branches/py3k-jit/JIT/jit_notes.txt
==============================================================================
--- (empty file)
+++ python/branches/py3k-jit/JIT/jit_notes.txt	Wed May 12 18:51:15 2010
@@ -0,0 +1,43 @@
+How CPython Uses LLVM to JIT
+============================
+
+This document tries to provide a high-level overview of how LLVM is used inside
+Python, including details of all the optimizations implemented for
+LLVM-generated Python machine code. This document should be as developer-centric
+as possible: it should be able to answer questions like, "how does Python
+determine function hotness" and also "where is that implemented?".
+
+Hotness model: finding critical functions
+-----------------------------------------
+
+TODO: Hotness is currently only measured, not acted on.
+
+We use an online model to estimate which functions are most critical to an
+application's performance. This model is as follows:
+
+- Each code object has a hotness level (the co_hotness field).
+    - For each function entry, add 10 to the hotness level.
+    - For each loop backedge, add 1 to the hotness level.
+- If the hotness level exceeds a given threshold (see ceval.c),
+  compile the code object to machine code via LLVM. This check is done on
+  function-entry and generator re-entry.
+
+There several classes of functions we're trying to catch with this model:
+
+- Straight-line utility functions (lots of invocations, low running time).
+- Loop-heavy main functions (few invocations, high running time).
+- Long-running generators (few invocations, long lifetime).
+
+Miscellaneous notes:
+- JIT compilation is always disabled during startup by temporarily forcing `-j
+  never`. This improves startup time by disabling compilation and feedback
+  collection.
+
+Previous models:
+- Simple call count-based model (10000 calls == hot). This was implemented as
+  an obviously-deficient baseline to be improved upon.
+- Previously, we didn't check code hotness on generator re-entry, which we
+  changed to catch long-running generators that are called once.
+
+Relevant Files:
+- Python/ceval.c - definition, use of the hotness model.

Modified: python/branches/py3k-jit/Lib/test/support.py
==============================================================================
--- python/branches/py3k-jit/Lib/test/support.py	(original)
+++ python/branches/py3k-jit/Lib/test/support.py	Wed May 12 18:51:15 2010
@@ -38,7 +38,7 @@
     "set_memlimit", "bigmemtest", "bigaddrspacetest", "BasicTestRunner",
     "run_unittest", "run_doctest", "threading_setup", "threading_cleanup",
     "reap_children", "cpython_only", "check_impl_detail", "get_attribute",
-    "swap_item", "swap_attr",
+    "swap_item", "swap_attr", "WITH_LLVM"
     ]
 
 
@@ -1222,3 +1222,8 @@
             yield
         finally:
             del obj[item]
+
+# WITH_LLVM is true if Python was compiled with LLVM support.
+def foo(): pass
+WITH_LLVM = hasattr(foo.__code__, "co_hotness")
+del foo

Modified: python/branches/py3k-jit/Lib/test/test_code.py
==============================================================================
--- python/branches/py3k-jit/Lib/test/test_code.py	(original)
+++ python/branches/py3k-jit/Lib/test/test_code.py	Wed May 12 18:51:15 2010
@@ -124,6 +124,25 @@
     print("consts:", tuple(consts(co.co_consts)))
 
 
+def new_code(function_name, def_string, globals_dict=None):
+    """Compiles function_name, defined in def_string into a new code object.
+
+    Compiles and runs def_string in a temporary namespace, with the specified
+    globals dict if any, and returns the function named 'function_name' out of
+    that namespace.
+
+    This allows us to track things that change in a code object as it's called
+    repeatedly.  Simply defining a local function would re-use the same code
+    object for each function
+
+    """
+    namespace = {}
+    if globals_dict is None:
+        globals_dict = {}
+    exec(def_string, globals_dict, namespace)
+    return namespace[function_name]
+
+
 class CodeTest(unittest.TestCase):
 
     def test_newempty(self):
@@ -133,6 +152,121 @@
         self.assertEquals(co.co_firstlineno, 15)
 
 
+HOTNESS_CALL = 10
+HOTNESS_ITER = 1
+
+ at unittest.skipUnless(hasattr(new_code.__code__, "co_hotness"),
+                     "Only applies with LLVM compiled in.")
+class HotnessTest(unittest.TestCase):
+
+    def setUp(self):
+        self.while_loop = new_code("while_loop", """
+def while_loop(x):
+  while x > 0:
+    x = x - 1
+""")
+
+    def test_new_code_has_0_hotness(self):
+        self.assertEquals(self.while_loop.__code__.co_hotness, 0)
+
+    def test_call_adds_10_hotness(self):
+        self.while_loop(0)
+        self.assertEquals(self.while_loop.__code__.co_hotness, HOTNESS_CALL)
+        self.while_loop(0)
+        self.assertEquals(self.while_loop.__code__.co_hotness, 2 * HOTNESS_CALL)
+
+        list(map(self.while_loop, [0]))  # Don't go through fast_function.
+        self.assertEquals(self.while_loop.__code__.co_hotness, 3 * HOTNESS_CALL)
+
+        kwargs = new_code("kwargs", """
+def kwargs(**kwa):
+  return kwa
+""")
+        self.assertEquals(kwargs.__code__.co_hotness, 0)
+        kwargs(a=3, b=4)  # Also doesn't go through fast_function.
+        self.assertEquals(kwargs.__code__.co_hotness, HOTNESS_CALL)
+
+    def test_iteration_adds_1_hotness(self):
+        self.while_loop(1)
+        self.assertEquals(self.while_loop.__code__.co_hotness,
+                          HOTNESS_CALL + HOTNESS_ITER)
+        self.while_loop(36)
+        self.assertEquals(self.while_loop.__code__.co_hotness,
+                          2 * HOTNESS_CALL + 37 * HOTNESS_ITER)
+
+        for_loop = new_code("for_loop", """
+def for_loop():
+  for x in range(17):
+    pass
+""")
+        self.assertEquals(for_loop.__code__.co_hotness, 0)
+        for_loop()
+        self.assertEquals(for_loop.__code__.co_hotness,
+                          HOTNESS_CALL + 17 * HOTNESS_ITER)
+
+    def test_nested_for_loop_hotness(self):
+        # Verify our understanding of how the hotness model deals with nested
+        # for loops. This can be confusing, and we don't want to change it
+        # accidentally.
+        foo = new_code("foo", """
+def foo():
+    for x in range(50):
+        for y in range(70):
+            pass
+""")
+        self.assertEqual(foo.__code__.co_hotness, 0)
+        foo()
+        self.assertEqual(foo.__code__.co_hotness,
+                         HOTNESS_CALL + HOTNESS_ITER * 3500 +
+                         HOTNESS_ITER * 50)
+
+    def test_for_loop_jump_threading_hotness(self):
+        # Regression test: the bytecode peephole optimizer does some limited
+        # jump threading, which caused problems for one earlier attempt at
+        # tuning the hotness model.
+        foo = new_code("foo", """
+def foo():
+    for x in range(30):
+        if x % 2:  # Alternate between the two branches
+            x = 8  # Nonsense
+""")
+        self.assertEqual(foo.__code__.co_hotness, 0)
+        foo()
+
+        hotness = HOTNESS_CALL + HOTNESS_ITER * 30
+
+    def test_early_for_loop_exit_hotness(self):
+        # Make sure we understand how the hotness model counts early exits from
+        # for loops.
+        foo = new_code("foo", """
+def foo():
+    for x in range(1000):
+        return True
+""")
+        self.assertEqual(foo.__code__.co_hotness, 0)
+        foo()
+
+        # Note that we don't count the loop in any way, since we never take
+        # a loop backedge.
+        self.assertEqual(foo.__code__.co_hotness, HOTNESS_CALL)
+
+    def test_generator_hotness(self):
+        foo = new_code("foo", """
+def foo():
+    yield 5
+    yield 6
+""")
+        # Generator object created, but not run yet.  This counts as the call.
+        l = foo()
+        self.assertEqual(foo.__code__.co_hotness, HOTNESS_CALL)
+
+        next(l)  # Enter the generator.  This is not a call.
+        self.assertEqual(foo.__code__.co_hotness, HOTNESS_CALL)
+        next(l)  # Neither is this.
+        self.assertEqual(foo.__code__.co_hotness, HOTNESS_CALL)
+
+
+
 class CodeWeakRefTest(unittest.TestCase):
 
     def test_basic(self):
@@ -162,7 +296,7 @@
     from test.support import run_doctest, run_unittest
     from test import test_code
     run_doctest(test_code, verbose)
-    run_unittest(CodeTest, CodeWeakRefTest)
+    run_unittest(CodeTest, HotnessTest, CodeWeakRefTest)
 
 
 if __name__ == "__main__":

Modified: python/branches/py3k-jit/Lib/test/test_sys.py
==============================================================================
--- python/branches/py3k-jit/Lib/test/test_sys.py	(original)
+++ python/branches/py3k-jit/Lib/test/test_sys.py	Wed May 12 18:51:15 2010
@@ -6,6 +6,7 @@
 import textwrap
 import warnings
 import operator
+from test.support import WITH_LLVM
 
 # count the number of test runs, used to create unique
 # strings to intern in test_intern()
@@ -593,7 +594,10 @@
             return inner
         check(get_cell().__closure__[0], size(h + 'P'))
         # code
-        check(get_cell().__code__, size(h + '5i8Pi3P'))
+        if WITH_LLVM:
+            check(get_cell().__code__, size(h + '5i8Pi3Pl'))
+        else:
+            check(get_cell().__code__, size(h + '5i8Pi3P'))
         # complex
         check(complex(0,1), size(h + '2d'))
         # method_descriptor (descriptor object)

Modified: python/branches/py3k-jit/Objects/codeobject.c
==============================================================================
--- python/branches/py3k-jit/Objects/codeobject.c	(original)
+++ python/branches/py3k-jit/Objects/codeobject.c	Wed May 12 18:51:15 2010
@@ -109,6 +109,9 @@
         co->co_lnotab = lnotab;
         co->co_zombieframe = NULL;
         co->co_weakreflist = NULL;
+#ifdef WITH_LLVM
+        co->co_hotness = 0;
+#endif  /* WITH_LLVM */
     }
     return co;
 }
@@ -179,6 +182,9 @@
     {"co_name",         T_OBJECT,       OFF(co_name),           READONLY},
     {"co_firstlineno", T_INT,           OFF(co_firstlineno),    READONLY},
     {"co_lnotab",       T_OBJECT,       OFF(co_lnotab),         READONLY},
+#ifdef WITH_LLVM
+    {"co_hotness",      T_INT,          OFF(co_hotness),        READONLY},
+#endif
     {NULL}      /* Sentinel */
 };
 

Modified: python/branches/py3k-jit/Python/ceval.c
==============================================================================
--- python/branches/py3k-jit/Python/ceval.c	(original)
+++ python/branches/py3k-jit/Python/ceval.c	Wed May 12 18:51:15 2010
@@ -116,6 +116,11 @@
                                       PyObject *);
 static PyObject * update_star_args(int, int, PyObject *, PyObject ***);
 static PyObject * load_args(PyObject ***, int);
+
+#ifdef WITH_LLVM
+static inline void mark_called(PyCodeObject *co);
+#endif  /* WITH_LLVM */
+
 #define CALL_FLAG_VAR 1
 #define CALL_FLAG_KW 2
 
@@ -963,6 +968,14 @@
 #define JUMPTO(x)       (next_instr = first_instr + (x))
 #define JUMPBY(x)       (next_instr += (x))
 
+/* Feedback-gathering macros */
+#ifdef WITH_LLVM
+#define UPDATE_HOTNESS_JABS() \
+    do { if (oparg <= f->f_lasti) ++co->co_hotness; } while (0)
+#else
+#define UPDATE_HOTNESS_JABS()
+#endif  /* WITH_LLVM */
+
 /* OpCode prediction macros
     Some opcodes tend to come in pairs thus making it possible to
     predict the second code when the first is run.  For example,
@@ -2373,6 +2386,7 @@
             }
             if (w == Py_False) {
                 Py_DECREF(w);
+                UPDATE_HOTNESS_JABS();
                 JUMPTO(oparg);
                 FAST_DISPATCH();
             }
@@ -2380,8 +2394,10 @@
             Py_DECREF(w);
             if (err > 0)
                 err = 0;
-            else if (err == 0)
+            else if (err == 0) {
+                UPDATE_HOTNESS_JABS();
                 JUMPTO(oparg);
+            }
             else
                 break;
             DISPATCH();
@@ -2395,6 +2411,7 @@
             }
             if (w == Py_True) {
                 Py_DECREF(w);
+                UPDATE_HOTNESS_JABS();
                 JUMPTO(oparg);
                 FAST_DISPATCH();
             }
@@ -2402,6 +2419,7 @@
             Py_DECREF(w);
             if (err > 0) {
                 err = 0;
+                UPDATE_HOTNESS_JABS();
                 JUMPTO(oparg);
             }
             else if (err == 0)
@@ -2418,6 +2436,7 @@
                 FAST_DISPATCH();
             }
             if (w == Py_False) {
+                UPDATE_HOTNESS_JABS();
                 JUMPTO(oparg);
                 FAST_DISPATCH();
             }
@@ -2427,8 +2446,10 @@
                 Py_DECREF(w);
                 err = 0;
             }
-            else if (err == 0)
+            else if (err == 0) {
+                UPDATE_HOTNESS_JABS();
                 JUMPTO(oparg);
+            }
             else
                 break;
             DISPATCH();
@@ -2441,12 +2462,14 @@
                 FAST_DISPATCH();
             }
             if (w == Py_True) {
+                UPDATE_HOTNESS_JABS();
                 JUMPTO(oparg);
                 FAST_DISPATCH();
             }
             err = PyObject_IsTrue(w);
             if (err > 0) {
                 err = 0;
+                UPDATE_HOTNESS_JABS();
                 JUMPTO(oparg);
             }
             else if (err == 0) {
@@ -2459,6 +2482,7 @@
 
         PREDICTED_WITH_ARG(JUMP_ABSOLUTE);
         TARGET(JUMP_ABSOLUTE)
+            UPDATE_HOTNESS_JABS();
             JUMPTO(oparg);
 #if FAST_LOOPS
             /* Enabling this path speeds-up all while and for-loops by bypassing
@@ -2514,6 +2538,9 @@
             goto fast_block_end;
 
         TARGET(CONTINUE_LOOP)
+#ifdef WITH_LLVM
+            ++co->co_hotness;
+#endif
             retval = PyLong_FromLong(oparg);
             if (!retval) {
                 x = NULL;
@@ -3080,6 +3107,13 @@
     if (f == NULL)
         return NULL;
 
+#ifdef WITH_LLVM
+    /* This is where a code object is considered "called". Doing it here
+     * instead of PyEval_EvalFrame() makes support for generators somewhat
+     * cleaner. */
+    mark_called(co);
+#endif  /* WITH_LLVM */
+
     fastlocals = f->f_localsplus;
     freevars = f->f_localsplus + co->co_nlocals;
 
@@ -3793,6 +3827,14 @@
                      nargs);
 }
 
+#ifdef WITH_LLVM
+static inline void
+mark_called(PyCodeObject *co)
+{
+    co->co_hotness += 10;
+}
+#endif  /* WITH_LLVM */
+
 #define C_TRACE(x, call) \
 if (tstate->use_tracing && tstate->c_profilefunc) { \
     if (call_trace(tstate->c_profilefunc, \
@@ -3947,6 +3989,9 @@
         f = PyFrame_New(tstate, co, globals, NULL);
         if (f == NULL)
             return NULL;
+#ifdef WITH_LLVM
+        mark_called(co);
+#endif
 
         fastlocals = f->f_localsplus;
         stack = (*pp_stack) - n;


More information about the Python-checkins mailing list