Explorar el Código

add fast path

blueloveTH hace 10 horas
padre
commit
9af48b8664
Se han modificado 3 ficheros con 126 adiciones y 11 borrados
  1. 3 1
      docs/features/ub.md
  2. 43 9
      src/interpreter/ceval.c
  3. 80 1
      tests/010_int.py

+ 3 - 1
docs/features/ub.md

@@ -5,7 +5,9 @@ title: Undefined Behaviour
 
 These are the undefined behaviours of pkpy. The behaviour of pkpy is undefined if you do the following things.
 
-1. Delete a builtin object. For example, `del int.__add__`.
+1. Modify or delete an attribute of a builtin type. For example, `int.__add__ = f` or
+   `del int.__add__`. The interpreter takes fast paths that assume the arithmetic and
+   comparison methods of `int` and `float` are the original ones.
 2. Call an unbound method with the wrong type of `self`. For example, `int.__add__('1', 2)`.
 3. Type `T`'s `__new__` returns an object that is not an instance of `T`.
 4. Call `__new__` with a type that is not a subclass of `type`.

+ 43 - 9
src/interpreter/ceval.c

@@ -81,6 +81,14 @@ static bool unpack_dict_to_buffer(py_Ref key, py_Ref val, void* ctx) {
     return TypeError("keywords must be strings, not '%t'", key->type);
 }
 
+static bool binaryop_isnum(const py_TValue* v) {
+    return v->type == tp_int || v->type == tp_float;
+}
+
+static py_f64 binaryop_tof64(const py_TValue* v) {
+    return v->type == tp_int ? (py_f64)v->_i64 : v->_f64;
+}
+
 FrameResult VM__run_top_frame(VM* self) {
     py_Frame* frame = self->top_frame;
     Bytecode* co_codes;
@@ -659,9 +667,34 @@ __NEXT_STEP:
         *TOP() = self->last_retval;                                                                \
         DISPATCH();                                                                                \
     }
-            CASE_BINARY_OP(OP_BINARY_ADD, __add__, __radd__)
-            CASE_BINARY_OP(OP_BINARY_SUB, __sub__, __rsub__)
-            CASE_BINARY_OP(OP_BINARY_MUL, __mul__, __rmul__)
+// Fast paths for `int`/`float` operands. These mirror `DEF_NUM_BINARY_OP` in
+// `py_number.c`, including the promotion of a mixed `int`/`float` pair. Modifying a
+// builtin type's magic methods is undefined behaviour (docs/features/ub.md), so the
+// type is never consulted here.
+#define CASE_BINARY_OP_NUM(label, op, rop, c_op, mk_i, mk_f)                                       \
+    case label: {                                                                                  \
+        if(SECOND()->type == tp_int && TOP()->type == tp_int) {                                    \
+            py_i64 lhs = SECOND()->_i64;                                                           \
+            py_i64 rhs = TOP()->_i64;                                                              \
+            POP();                                                                                 \
+            mk_i(TOP(), lhs c_op rhs);                                                             \
+            DISPATCH();                                                                            \
+        }                                                                                          \
+        if(binaryop_isnum(SECOND()) && binaryop_isnum(TOP())) {                                    \
+            py_f64 lhs = binaryop_tof64(SECOND());                                                 \
+            py_f64 rhs = binaryop_tof64(TOP());                                                    \
+            POP();                                                                                 \
+            mk_f(TOP(), lhs c_op rhs);                                                             \
+            DISPATCH();                                                                            \
+        }                                                                                          \
+        if(!pk_stack_binaryop(self, op, rop)) goto __ERROR;                                        \
+        POP();                                                                                     \
+        *TOP() = self->last_retval;                                                                \
+        DISPATCH();                                                                                \
+    }
+            CASE_BINARY_OP_NUM(OP_BINARY_ADD, __add__, __radd__, +, py_newint, py_newfloat)
+            CASE_BINARY_OP_NUM(OP_BINARY_SUB, __sub__, __rsub__, -, py_newint, py_newfloat)
+            CASE_BINARY_OP_NUM(OP_BINARY_MUL, __mul__, __rmul__, *, py_newint, py_newfloat)
             CASE_BINARY_OP(OP_BINARY_TRUEDIV, __truediv__, __rtruediv__)
             CASE_BINARY_OP(OP_BINARY_FLOORDIV, __floordiv__, __rfloordiv__)
             CASE_BINARY_OP(OP_BINARY_MOD, __mod__, __rmod__)
@@ -672,13 +705,14 @@ __NEXT_STEP:
             CASE_BINARY_OP(OP_BINARY_OR, __or__, 0)
             CASE_BINARY_OP(OP_BINARY_XOR, __xor__, 0)
             CASE_BINARY_OP(OP_BINARY_MATMUL, __matmul__, 0)
-            CASE_BINARY_OP(OP_COMPARE_LT, __lt__, __gt__)
-            CASE_BINARY_OP(OP_COMPARE_LE, __le__, __ge__)
-            CASE_BINARY_OP(OP_COMPARE_EQ, __eq__, __eq__)
-            CASE_BINARY_OP(OP_COMPARE_NE, __ne__, __ne__)
-            CASE_BINARY_OP(OP_COMPARE_GT, __gt__, __lt__)
-            CASE_BINARY_OP(OP_COMPARE_GE, __ge__, __le__)
+            CASE_BINARY_OP_NUM(OP_COMPARE_LT, __lt__, __gt__, <, py_newbool, py_newbool)
+            CASE_BINARY_OP_NUM(OP_COMPARE_LE, __le__, __ge__, <=, py_newbool, py_newbool)
+            CASE_BINARY_OP_NUM(OP_COMPARE_EQ, __eq__, __eq__, ==, py_newbool, py_newbool)
+            CASE_BINARY_OP_NUM(OP_COMPARE_NE, __ne__, __ne__, !=, py_newbool, py_newbool)
+            CASE_BINARY_OP_NUM(OP_COMPARE_GT, __gt__, __lt__, >, py_newbool, py_newbool)
+            CASE_BINARY_OP_NUM(OP_COMPARE_GE, __ge__, __le__, >=, py_newbool, py_newbool)
 #undef CASE_BINARY_OP
+#undef CASE_BINARY_OP_NUM
         case OP_IS_OP: {
             bool res = py_isidentical(SECOND(), TOP());
             POP();

+ 80 - 1
tests/010_int.py

@@ -871,4 +871,83 @@ assert 9 // 7 == 1
 assert 9 % 8 == 1
 assert 9 // 8 == 1
 assert 9 % 9 == 0
-assert 9 // 9 == 1
+assert 9 // 9 == 1
+
+# `+`, `-`, `*` and the comparisons take an inline fast path when both operands
+# are `int` or `float`. Cover every type pairing and every way out of it.
+i, f = 7, 2.5
+
+# int op int -> int
+assert 7 + 2 == 9 and type(7 + 2) is int
+assert 7 - 2 == 5 and type(7 - 2) is int
+assert 7 * 2 == 14 and type(7 * 2) is int
+
+# float op float -> float
+assert 7.5 + 2.5 == 10.0 and type(7.5 + 2.5) is float
+assert 7.5 - 2.5 == 5.0 and type(7.5 - 2.5) is float
+assert 7.5 * 2.0 == 15.0 and type(7.5 * 2.0) is float
+
+# mixed operands promote to float in both directions
+assert i + f == 9.5 and type(i + f) is float
+assert f + i == 9.5 and type(f + i) is float
+assert i - f == 4.5 and type(i - f) is float
+assert f - i == -4.5 and type(f - i) is float
+assert i * f == 17.5 and type(i * f) is float
+assert f * i == 17.5 and type(f * i) is float
+
+# comparisons, all four pairings
+assert (2 < 7) and not (7 < 2)
+assert (2.5 < 7.5) and not (7.5 < 2.5)
+assert (2 < 7.5) and not (7.5 < 2)
+assert (2.5 < 7) and not (7 < 2.5)
+assert (7 >= 7) and (7.0 >= 7) and (7 >= 7.0) and (7.0 >= 7.0)
+assert (7 == 7.0) and (7.0 == 7) and not (7 != 7.0)
+assert (7 <= 7.0) and (7.0 <= 7) and not (7 > 7.0) and not (7.0 > 7)
+
+# `bool` is a separate type here, not a subclass of `int`, so it must fall
+# through to the generic path and reach `bool.__add__` and friends
+assert True + 2 == 3
+assert 2 + True == 3
+assert True - 1 == 0
+assert 2 * True == 2
+
+# non-numbers still reach their own magic methods
+assert 'a' + 'b' == 'ab'
+assert [1] + [2] == [1, 2]
+assert 'a' * 2 == 'aa'
+assert (1, 2) < (1, 3)
+
+class Vec:
+    def __init__(self, v):
+        self.v = v
+    def __add__(self, other):
+        return Vec(self.v + other.v)
+    def __radd__(self, other):
+        return Vec(self.v + other)
+    def __lt__(self, other):
+        return self.v < other.v
+
+assert (Vec(1) + Vec(2)).v == 3
+assert (2 + Vec(1)).v == 3          # int.__add__ returns NotImplemented -> __radd__
+assert Vec(1) < Vec(2)
+
+# unsupported pairings still raise
+try:
+    1 + 'a'
+    exit(1)
+except TypeError:
+    pass
+try:
+    1 < 'a'
+    exit(1)
+except TypeError:
+    pass
+
+# division is deliberately not on the fast path
+assert 7 / 2 == 3.5 and type(7 / 2) is float
+assert 7 // 2 == 3 and 7 % 2 == 1
+try:
+    1 / 0
+    exit(1)
+except ZeroDivisionError:
+    pass