Skip to main content

Python/ceval.c (part 98)

Source:

cpython 3.14 @ ab2d84fe1023/Python/ceval.c

This annotation covers arithmetic specializations. See python_ceval97_detail for FOR_ITER specializations.

Map

LinesSymbolRole
1-80BINARY_OPGeneric binary operation with inline cache
81-180BINARY_OP_ADD_INTint + int fast path
181-280BINARY_OP_MULTIPLY_INTint * int fast path
281-380BINARY_OP_ADD_FLOATfloat + float fast path
381-500BINARY_OP_ADD_UNICODEstr + str fast path

Reading

BINARY_OP

// CPython: Python/ceval.c:2220 BINARY_OP
inst(BINARY_OP, (lhs, rhs -- result)) {
_PyBinaryOpCache *cache = (_PyBinaryOpCache *)next_instr;
if (ADAPTIVE_COUNTER_TRIGGERS(cache->counter)) {
_Py_Specialize_BinaryOp(lhs, rhs, next_instr, oparg, &GETLOCAL(0));
DISPATCH_SAME_OPARG();
}
result = binary_ops[oparg](lhs, rhs);
ERROR_IF(result == NULL, error);
Py_DECREF(lhs);
Py_DECREF(rhs);
}

BINARY_OP is the generic path. binary_ops[oparg] is a table of function pointers indexed by the operator (NB_ADD, NB_MULTIPLY, etc.). After warmup, _Py_Specialize_BinaryOp replaces the opcode with a type-specific specialization.

BINARY_OP_ADD_INT

// CPython: Python/ceval.c:2280 BINARY_OP_ADD_INT
inst(BINARY_OP_ADD_INT, (left, right -- result)) {
DEOPT_IF(!PyLong_CheckExact(left), BINARY_OP);
DEOPT_IF(!PyLong_CheckExact(right), BINARY_OP);
STAT_INC(BINARY_OP, hit);
/* Try compact int path first (no memory allocation) */
result = _PyLong_Add((PyLongObject *)left, (PyLongObject *)right);
ERROR_IF(result == NULL, error);
Py_DECREF(left);
Py_DECREF(right);
}

BINARY_OP_ADD_INT avoids PyNumber_Add's type dispatch: it calls _PyLong_Add directly. _PyLong_Add has a fast path for "compact" integers (those that fit in a single digit, i.e., |x| < 2^30) that avoids allocation when the result is also compact.

BINARY_OP_MULTIPLY_INT

// CPython: Python/ceval.c:2310 BINARY_OP_MULTIPLY_INT
inst(BINARY_OP_MULTIPLY_INT, (left, right -- result)) {
DEOPT_IF(!PyLong_CheckExact(left), BINARY_OP);
DEOPT_IF(!PyLong_CheckExact(right), BINARY_OP);
STAT_INC(BINARY_OP, hit);
result = _PyLong_Multiply((PyLongObject *)left, (PyLongObject *)right);
ERROR_IF(result == NULL, error);
Py_DECREF(left);
Py_DECREF(right);
}

int * int with small integers is also common (loop counters, index arithmetic). _PyLong_Multiply has a single-digit fast path that computes the product and stores it as a compact integer without heap allocation.

BINARY_OP_ADD_FLOAT

// CPython: Python/ceval.c:2350 BINARY_OP_ADD_FLOAT
inst(BINARY_OP_ADD_FLOAT, (left, right -- result)) {
DEOPT_IF(!PyFloat_CheckExact(left), BINARY_OP);
DEOPT_IF(!PyFloat_CheckExact(right), BINARY_OP);
STAT_INC(BINARY_OP, hit);
double dsum = ((PyFloatObject *)left)->ob_fval + ((PyFloatObject *)right)->ob_fval;
result = PyFloat_FromDouble(dsum);
ERROR_IF(result == NULL, error);
Py_DECREF(left);
Py_DECREF(right);
}

float + float reads ob_fval directly and adds with +. PyFloat_FromDouble allocates a new float object — there is no float free list in Python 3.12+ (removed in favor of the immortal float scheme for small values in later versions).

BINARY_OP_ADD_UNICODE

// CPython: Python/ceval.c:2400 BINARY_OP_ADD_UNICODE
inst(BINARY_OP_ADD_UNICODE, (left, right -- result)) {
DEOPT_IF(!PyUnicode_CheckExact(left), BINARY_OP);
DEOPT_IF(!PyUnicode_CheckExact(right), BINARY_OP);
STAT_INC(BINARY_OP, hit);
result = PyUnicode_Concat(left, right);
ERROR_IF(result == NULL, error);
Py_DECREF(left);
Py_DECREF(right);
}

str + str calls PyUnicode_Concat directly, bypassing PyNumber_Add's dispatch. For the a += b pattern, a separate specialization BINARY_OP_INPLACE_ADD_UNICODE checks for single-owner strings and resizes in-place.

gopy notes

BINARY_OP is in vm/eval_gen.go. BINARY_OP_ADD_INT checks objects.IsCompactInt before calling objects.IntAddCompact (no allocation path). BINARY_OP_ADD_FLOAT reads objects.Float.Val directly. BINARY_OP_ADD_UNICODE calls objects.StrConcat. BINARY_OP_MULTIPLY_INT calls objects.IntMulCompact.