Skip to main content

Python/ceval.c (part 19)

Source:

cpython 3.14 @ ab2d84fe1023/Python/ceval.c

This annotation covers binary operator specializations. See python_ceval18_detail for subscript specializations and python_ceval17_detail for call opcodes.

Map

LinesSymbolRole
1-80BINARY_OP_ADD_INTint + int without tp_as_number dispatch
81-160BINARY_OP_ADD_FLOATfloat + float directly on ob_fval
161-260BINARY_OP_MULTIPLY_INTint * int compact fast path
261-380BINARY_OP_MULTIPLY_FLOATfloat * float direct fmul
381-500BINARY_OP_INPLACE_ADD_UNICODEstr += str resize-in-place optimization

Reading

BINARY_OP_ADD_INT

// CPython: Python/ceval.c:2380 BINARY_OP_ADD_INT
inst(BINARY_OP_ADD_INT, (left, right -- res)) {
DEOPT_IF(!PyLong_CheckExact(left), BINARY_OP);
DEOPT_IF(!PyLong_CheckExact(right), BINARY_OP);
DEOPT_IF(!_PyLong_IsCompact((PyLongObject *)left), BINARY_OP);
DEOPT_IF(!_PyLong_IsCompact((PyLongObject *)right), BINARY_OP);
/* Both are compact (fit in one digit): use direct addition */
Py_ssize_t lv = _PyLong_CompactValue((PyLongObject *)left);
Py_ssize_t rv = _PyLong_CompactValue((PyLongObject *)right);
/* Check for overflow */
res = _PyLong_FromSTwoDigits(lv + rv);
DECREMENT_ADAPTIVE_COUNTER(this_instr[-1].cache);
}

Compact integers (CPython 3.12+) store the value directly in the PyLongObject.long_value.ob_digit[0] when it fits in Py_ssize_t. The specialization bypasses long_add and avoids digit array arithmetic entirely. Most loop counters and small integer arithmetic hits this path.

BINARY_OP_ADD_FLOAT

// CPython: Python/ceval.c:2440 BINARY_OP_ADD_FLOAT
inst(BINARY_OP_ADD_FLOAT, (left, right -- res)) {
DEOPT_IF(!PyFloat_CheckExact(left), BINARY_OP);
DEOPT_IF(!PyFloat_CheckExact(right), BINARY_OP);
double dv = ((PyFloatObject *)left)->ob_fval +
((PyFloatObject *)right)->ob_fval;
/* Attempt to reuse right object if refcount == 1 */
DECREF_SPECIALIZED(right, _PyFloat_ExactDealloc);
res = _PyFloat_FromDoubleConsume(left, dv);
}

_PyFloat_FromDoubleConsume reuses the left float object if its reference count is 1, avoiding an allocation. This matters in tight arithmetic loops where temporaries are immediately discarded.

BINARY_OP_MULTIPLY_INT

// CPython: Python/ceval.c:2500 BINARY_OP_MULTIPLY_INT
inst(BINARY_OP_MULTIPLY_INT, (left, right -- res)) {
DEOPT_IF(!PyLong_CheckExact(left), BINARY_OP);
DEOPT_IF(!PyLong_CheckExact(right), BINARY_OP);
DEOPT_IF(!_PyLong_IsCompact((PyLongObject *)left), BINARY_OP);
DEOPT_IF(!_PyLong_IsCompact((PyLongObject *)right), BINARY_OP);
Py_ssize_t lv = _PyLong_CompactValue((PyLongObject *)left);
Py_ssize_t rv = _PyLong_CompactValue((PyLongObject *)right);
/* __int128 or overflow-checked multiplication */
res = _PyLong_FromLongLong((__int128)lv * rv);
DECREMENT_ADAPTIVE_COUNTER(this_instr[-1].cache);
}

On platforms with __int128, the product is computed without overflow by widening to 128 bits and then checking if the result fits back into compact range. If it overflows, the result is a multi-digit long but still correct.

BINARY_OP_INPLACE_ADD_UNICODE

// CPython: Python/ceval.c:2600 BINARY_OP_INPLACE_ADD_UNICODE
inst(BINARY_OP_INPLACE_ADD_UNICODE, (left, right -- res)) {
DEOPT_IF(!PyUnicode_CheckExact(left), BINARY_OP);
DEOPT_IF(!PyUnicode_CheckExact(right), BINARY_OP);
/* If left has refcount == 1 and is the accumulator variable,
resize in place to avoid copying the prefix each iteration */
if (Py_REFCNT(left) == 1) {
if (PyUnicode_Append(&left, right) < 0) ERROR_IF(true, error);
res = left;
} else {
res = PyUnicode_Concat(left, right);
}
STACK_SHRINK(1);
}

s += chunk in a loop uses PyUnicode_Append which can resize the string object in place when it is the sole reference. This transforms an O(n^2) concatenation loop into an amortized O(n) operation, matching the behavior of str.join for the common accumulation pattern.

gopy notes

BINARY_OP_ADD_INT is vm.BinaryOpAddInt in vm/eval_specialize.go. The compact integer fast path checks objects.LongIsCompact. BINARY_OP_ADD_FLOAT reuse is implemented by checking the refcount before calling objects.FloatFromDouble. BINARY_OP_INPLACE_ADD_UNICODE calls objects.UnicodeAppend which uses append([]rune, ...) and re-wraps in an interned string.