SimplifyC++ Article
9 Seconds vs. 514 Seconds: Why Was C++ with Clang About 57 Times Faster Than Python?

A Case Study in Software Performance
Nine Seconds vs. 514 Seconds
Why Did C++ with Clang Outperform Python by About 57 Times?
A few lines of source code can look remarkably similar yet take dramatically different amounts of time to execute. This example shows how integer representation, execution mechanisms, and compiler optimizations affect a cost repeated ten billion times.
Understanding the Numbers
The C++ program took nine seconds, while the Python program took approximately eight minutes and thirty-four seconds. Dividing the execution times gives:
| Metric | C++ / Clang | Python |
|---|---|---|
| Execution time | 9 seconds | 514 seconds |
| Average time per source-level iteration | 0.9 ns | 51.4 ns |
| Source-level iterations per second | Approximately 1.11 billion | Approximately 19.46 million |
| Time saved per execution | 505 seconds: eight minutes and 25 seconds | |
This is a speedup of approximately 57 times in this particular test.
It does not establish a universal ratio for all programs written in
these languages. Furthermore, 0.9 ns is an average calculated
per iteration written in the source code. It is neither the execution
time of a single machine instruction nor proof that the compiler
preserved the source-level iterations unchanged.
What Does the Program Actually Measure?
Both programs count from 1 to 10,000,000,000 and accumulate
the integers sequentially. Each iteration increments the counter,
adds it to the low part of the sum, checks whether the base has been
reached, and decides whether to continue the loop. The test therefore
emphasizes the cost of a sequential arithmetic loop running on one
thread. There are no large arrays, network requests, file reads,
or print operations inside the loop.
At the source-code level, both algorithms perform O(N)
iterations and maintain a small, fixed live working set for this
numeric range. Equal iteration counts, however, do not mean equal
amounts of internal work in each execution environment. In the
source-level algorithm, the normalization condition is checked ten
billion times, while high increases only fifty times.
Why Split the Sum into high and low?
S = 50,000,000,005,000,000,000
The result exceeds the maximum value of int64_t,
which is 9,223,372,036,854,775,807. It also exceeds the
maximum value of uint64_t. The example stores the result
using a representation with a large decimal base:
high = 50 ; low = 5,000,000,000
At the beginning of each iteration, low < 10¹⁸.
With the specified counter limit, the intermediate addition remains
below 10¹⁸ + 10¹⁰, which fits within a signed 64-bit integer.
A single subtraction is sufficient to normalize the result in this
experiment. This correctness argument depends on the example's bounds;
it does not guarantee correctness for every future value of
limit. Signed integer overflow in C++ has undefined behavior.
[14]
Why Can C++ Produce a Lightweight Execution Path?
The source code specifies in advance that the variables are 64-bit integers. In an optimized build, Clang can keep the loop state in CPU registers and generate arithmetic, comparison, selection, or branch instructions appropriate for the target. The loop body does not request dynamically allocated objects. These are properties inferred from the code; identifying the actual registers and instructions requires inspecting the executable.
Optimization levels such as -O2 and -O3
enable multiple transformations. These may include function inlining,
constant propagation, instruction reduction, and loop restructuring
when legal and beneficial. Choosing -O3 enables additional
optimizations, but does not guarantee that it will outperform
-O2 in every case.
[6]
It is also misleading to assume that using C++ necessarily introduces overhead through classes or object-oriented programming. This example uses a structure containing three values, with no inheritance, virtual calls, or dynamic allocation in the loop. The program's performance should be explained in terms of what it actually executes.
What Additional Work Does CPython Perform?
1. Intermediate Instructions Executed by the Interpreter
CPython compiles Python source code into bytecode. Its execution system then runs those instructions and the operations they invoke. It does not need to reread the source line on every iteration, yet the costs of executing intermediate instructions and handling values remain. The interpreter itself ultimately runs as machine code, but its execution structure adds layers of work around a direct arithmetic operation. [1]
2. Integers Are Objects with Variable Precision
In CPython, int is an arbitrary-precision integer represented
by a PyLongObject. In this C++ program,
std::int64_t is a fixed-width value. An expression such as
low + count follows a CPython execution path designed for
its integer and object model, even when the mathematical value would
fit within 64 bits.
[2]
3. Immutable Values and Object Lifetime Management
Python numbers are immutable. The expression
count = count + 1 rebinds the name to the resulting value.
Depending on the execution path, this may involve creating and
releasing integer objects or reusing existing ones. CPython's reference
management contributes to this cost. This does not mean that billions
of integers remain in memory, or that every addition makes a separate
allocation request to the operating system. The size of the live
working set must be distinguished from the volume of value-management
activity over time.
[3]
[4]
4. Flexible Semantics Require Broader Execution Paths
Python's object model allows operations on different types and supports customizable behavior. Adaptive specialization can narrow some execution paths when types stabilize, accelerating common operations. It would therefore be inaccurate to assume that every operation in modern releases always takes the slowest generic path. JIT settings also vary by version and build, so the configuration actually used in the experiment should be documented. [11] [12]
Function-local variables have fast access instructions such as
LOAD_FAST. The gap cannot be explained by claiming that
every access to count requires a dictionary lookup of its
name. Similarly, writing limit: int does not turn the
variable into a fixed-width machine integer under conventional
execution. It is a type hint useful for development tools and
static checking.
[10]
[5]
| Aspect | Optimized C++ | CPython |
|---|---|---|
| Numeric values | Fixed-width integers known to the compiler | Arbitrary-precision integer objects |
| Loop state | Can remain in CPU registers | Local variables and references within an execution frame |
| Addition | Native arithmetic instructions suited to the target | An execution path for integer objects, potentially specialized |
| Temporary value management | The code requests no dynamic allocation inside the loop | May involve creating values and managing references and lifetimes |
| Optimization scope | Build-time transformations of functions and loops | Depends on the interpreter, JIT, or compilation tool in use |
When the cost of each iteration increases by tens of nanoseconds and that cost is repeated ten billion times, hundreds of seconds accumulate. The large difference becomes understandable without assuming that the mathematical operation itself is harder in one language than in the other.
Insufficient Explanations and Unsupported Conclusions
- The GIL: It matters when discussing parallel execution across Python threads. This example is single-threaded in both languages, so the code contains no competition between threads that would explain the fundamental gap. Changing the GIL configuration does not guarantee the disappearance of these execution costs. [13]
- Constructing Totals: This happens once, when the function returns. Whatever the difference in its cost, it is unlikely to be the primary reason for a gap accumulated over ten billion iterations.
- Printing and reading the source file: Printing occurs outside the measured interval, and function definitions and imports occur before it. The 514 seconds cannot reasonably be attributed to output formatting or to reparsing the source text on every iteration.
- Memory and garbage collection: The program does not build a huge list of numbers. Managing integer-object lifetimes does not necessarily mean that the cyclic garbage collector runs on every iteration.
-
SIMD and AVX:
The dependence of
lowon its previous value, together with the normalization condition, makes the analysis more subtle than summing independent elements. LLVM's documentation describes vectorization of some reductions, but does not establish that this particular loop was vectorized. [8]
Likewise, declaring constexpr limit does not require the
function to be evaluated at compile time, although visible constants
may help optimization. C++'s observable-behavior rules allow the
implementation to restructure operations while preserving the required
result. Printing the result ensures that it is used; it does not,
by itself, prove that every source-level iteration remains in the
final program.
[7]
How Can the Comparison Be Made Reproducible?
The current result shows a substantial difference, and documenting the environment makes the conclusion more valuable. Record the processor model, operating system, Python and Clang versions, Python implementation, build options, and raw timings before rounding. Then calculate the ratio from those timings.
- Use the same machine, power mode, and comparable background load. Run the two measurements sequentially.
-
Document the optimized C++ build instead of relying on the project
configuration's name alone. State whether LTO or
-march=nativewas used. - Measure the interval around the function in both programs, excluding compilation and process startup. For a JIT, distinguish the first execution from execution after warm-up.
- Repeat the measurements as time permits and retain all results. The minimum time helps estimate performance with the least external interference; a median can also be included to show stability. Explain the measurement method. [18]
-
Verify the numerical results and inspect the assembly when
investigating the loop's instructions. To reduce input constant
propagation, the limit can be supplied at runtime, or the function
can be placed in a separate translation unit without LTO.
Then inspect the output. Adding
volatileto the variables changes the workload being measured.
time.monotonic_ns() and steady_clock are
suitable choices for measuring elapsed intervals. Reporting nanoseconds
does not guarantee an actual resolution of one nanosecond. The current
measurement is elapsed time and may therefore be affected by
operating-system scheduling.
[9]
[20]
Example Windows Commands with a Configured Clang Environment
python -VV
python -c "import platform; print(platform.python_implementation())"
clang++ --version
clang++ -std=c++20 -O3 count_sum.cpp -o count_sum.exe
.\count_sum.exe
python count_sum.py
clang++ -std=c++20 -O3 -S -masm=intel count_sum.cpp -o count_sum.s
The following command can be used to inspect bytecode. Instruction names and details vary across releases, and one bytecode instruction does not correspond to one machine instruction. [10]
python -c "import dis, count_sum; dis.dis(count_sum.count_sum)"
Where Is Each Language a Good Fit?
Efficiency includes execution time, memory use, and the costs of development, maintenance, and deployment. This test provides direct evidence about the efficiency of a specific arithmetic loop. It does not measure team productivity, network performance, or machine-learning libraries. The following guidance is an engineering assessment based on where the application's heavy work occurs.
| Domain | A Suitable Role for C++ | A Suitable Role for Python |
|---|---|---|
| Intensive numeric loops | Executing the computational kernel as native code | Selecting a better algorithm or directing work to a compiled kernel |
| Systems and developer tools | Resource control, compilers, and execution engines | Automating builds, checks, reports, and tests |
| Games, audio, and video | Time-sensitive components and processing engines | Content tools, preprocessing, and orchestration |
| Data, research, and machine learning | Implementing algorithms and high-performance kernels | Analysis, experimentation, workflow construction, and library integration |
| Backend services | Paths requiring low CPU cost or precise resource control | Business logic and integrations when performance limits suit the actual workload |
| Prototyping | Early testing of execution behavior and resource use when needed | Rapid expression, experimentation, and solution exploration |
C++ offers extensive control and makes developers responsible for correct resource use and numeric ranges. Python makes many solutions easier to build through flexibility and ready-made libraries, while requiring attention to the cost of paths executed inside the interpreter. In applications dominated by database or network waits, those delays or the service architecture may determine performance more strongly than the speed of this loop.
How Can Scientific Python Applications Be Fast?
NumPy performs many array operations in compiled code. Python therefore
pays the cost of invoking an operation instead of managing every element
through a Python loop. Tools such as Numba can compile suitable numeric
functions; that changes the execution mechanism being compared.
The function may need adaptation to return simple values instead of
a dataclass instance. When benchmarking a JIT, separate
the initial compilation time from steady-state execution time.
[15]
[17]
This example needs particular care: an array of ten billion elements,
at eight bytes per element, requires approximately 80 GB
in decimal units before additional memory costs. Accumulating the sum
in int64 also cannot represent the correct result.
Moving the work to NumPy or fixed-width arithmetic requires a design
that keeps intermediate and final results within range. Converting
to a floating-point type does not guarantee preservation of the
complete exact integer result.
[16]
The Fastest Solution May Begin with the Algorithm
If the goal is simply to obtain the sum, the mathematical formula removes the need to count one iteration at a time. Python can express the exact solution directly:
limit = 10_000_000_000
total = limit * (limit + 1) // 2
high, low = divmod(total, 10**18)
assert total == 50_000_000_005_000_000_000
assert (high, low) == (50, 5_000_000_000)
This uses a fixed number of arithmetic operations, although the cost of operations on large integers depends on their bit length. In C++, an appropriate type must be selected for the product and the result, such as an extended integer type supported on the target or a multiprecision library. Replacing the loop directly with 64-bit multiplication does not automatically preserve correctness.
Python using the formula can outperform a C++ program that still performs ten billion iterations. That is an algorithmic improvement. To compare loop-execution cost, retain the sequential algorithm in both programs. To compare the best solution to the problem, allow both programs to use the formula. Defining the question first prevents these two comparisons from being confused.
The engineering decision: Put repeated computation on an execution path that meets the time and resource budget, and use the language that makes the remaining work easier. The reported times of 9 and 514 seconds demonstrate the benefit of native execution for this loop in this experiment. Choosing the language for an entire application still depends on its real workload, architecture, and development cost.
The Complete, Formatted Programs
The original loop operations and values have been preserved. The Python version prints the numerically reconstructed sum, while the C++ version prints the reconstruction relationship used in the submitted example. Both print operations occur outside the measured interval.
count_sum.py — Python
import time
from dataclasses import dataclass
@dataclass
class Totals:
count: int
high: int
low: int
def count_sum(limit: int) -> Totals:
count = 0
high = 0
low = 0
base = 1_000_000_000_000_000_000
while count < limit:
count = count + 1
low = low + count
if low >= base:
low = low - base
high = high + 1
return Totals(count=count, high=high, low=low)
def main() -> int:
limit = 10_000_000_000
print("Starting: count and sum 1..10,000,000,000")
print("Please wait. Nothing is printed inside the timed loop.")
start = time.monotonic_ns()
result = count_sum(limit)
finish = time.monotonic_ns()
assert start >= 0 and finish > start, "invalid timer"
elapsed = finish - start
ms = elapsed // 1_000_000
seconds = elapsed / 1_000_000_000.0
ns_per_iteration = elapsed / limit
print(f"count = {result.count}")
print(f"sum_high = {result.high}; sum_low = {result.low}")
print(
f"Exact sum = "
f"{result.high * 1_000_000_000_000_000_000 + result.low}"
)
print("Expected sum = 50000000005000000000")
print(f"elapsed_ns = {elapsed}")
print(f"elapsed_ms = {ms}")
print(f"elapsed_seconds = {seconds}")
print(f"ns_per_iteration = {ns_per_iteration}")
assert (
result.count == limit
and result.high == 50
and result.low == 5_000_000_000
), "wrong result"
print("PASS")
return 0
if __name__ == "__main__":
raise SystemExit(main())
count_sum.cpp — C++ / Clang
#include <chrono>
#include <cstdint>
#include <iostream>
struct Totals {
std::int64_t count;
std::int64_t high;
std::int64_t low;
};
Totals count_sum(std::int64_t limit) {
std::int64_t count = 0, high = 0, low = 0;
constexpr std::int64_t base = 1000000000000000000LL;
while (count < limit) {
count = count + 1;
low = low + count;
if (low >= base) {
low = low - base;
high = high + 1;
}
}
return {count, high, low};
}
int main() {
constexpr std::int64_t limit = 10000000000LL;
std::cout
<< "Starting: count and sum 1..10,000,000,000\n"
<< "Please wait. Nothing is printed inside the timed loop.\n"
<< std::flush;
const auto start = std::chrono::steady_clock::now();
const auto result = count_sum(limit);
const auto finish = std::chrono::steady_clock::now();
const auto elapsed =
std::chrono::duration_cast<std::chrono::nanoseconds>(
finish - start
).count();
if (elapsed <= 0) return 2;
std::cout
<< "count = " << result.count
<< "\nsum_high = " << result.high
<< "; sum_low = " << result.low
<< "\nExact sum = sum_high * 1000000000000000000 + sum_low"
<< "\nExpected sum = 50000000005000000000"
<< "\nelapsed_ns = " << elapsed
<< "\nelapsed_ms = " << elapsed / 1000000
<< "\nelapsed_seconds = " << static_cast<double>(elapsed) / 1e9
<< "\nns_per_iteration = "
<< static_cast<double>(elapsed) / limit
<< '\n';
if (result.count != limit
|| result.high != 50
|| result.low != 5000000000LL) {
std::cerr << "FAIL: wrong result\n";
return 2;
}
std::cout << "PASS\n";
return 0;
}
The Python checks use assert, which may be removed when
running with -O. Run the example without that option when
relying on its assertions. The C++ version uses explicit
if statements for validation.
[19]
Technical References
The following sources are official tool documentation or the C++ working draft. The timings come from the example author's experiment; the calculations and discussion of the code's structure are an analysis of this specific case.
- CPython Documentation: Bytecode
- CPython Documentation: Integer Objects
- The Python Data Model: Objects and Immutable Numbers
- Reference Counting in CPython
- Python Documentation: The Meaning of Type Hints
- Clang Documentation: Optimization Levels and Assembly Output
- C++ Working Draft: Observable Behavior and the Abstract Machine
- LLVM: Automatic Vectorization
- Python Documentation: time and monotonic_ns
- Python Documentation: Bytecode and LOAD_FAST
- PEP 659: Specializing Adaptive Interpreter
- CPython Documentation: JIT Compilation and Configuration
- CPython Documentation: Thread States and the GIL
- C++ Working Draft: Integer Ranges and Overflow Behavior
- NumPy: Executing Array Operations in Compiled Code
- NumPy: Numeric Types and Overflow
- Numba: Loops and Nopython Mode
- timeit Documentation: Repeated Measurements and Excluding Setup
- Python Documentation: assert and the -O Option
- C++ Working Draft: steady_clock
What did you think?
Sign in to react or comment.
Comments
0No comments yet.