SimplifyC++ Article

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

By Ayman AlherakiReads: 12Today: 12

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.

9 s C++ with Clang
514 s Python
≈ 57.1× Speedup in this experiment
Scope of the evidence: These timings were reported by the author of the example; the experiment was not repeated here. The processor specifications, tool versions, and C++ build options were not provided. The analysis assumes conventional CPython execution and an optimized C++ build. Documenting the test environment is essential to making the comparison reproducible.

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:

Speedup = 514 ÷ 9 ≈ 57.1
Values derived from the reported timings for ten billion iterations
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 = N × (N + 1) ÷ 2
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:

S = high × 10¹⁸ + low
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]

Potential execution differences in a conventional implementation of the loop
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 low on 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.

  1. Use the same machine, power mode, and comparable background load. Run the two measurements sequentially.
  2. Document the optimized C++ build instead of relying on the project configuration's name alone. State whether LTO or -march=native was used.
  3. 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.
  4. 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]
  5. 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 volatile to 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.

  1. CPython Documentation: Bytecode
  2. CPython Documentation: Integer Objects
  3. The Python Data Model: Objects and Immutable Numbers
  4. Reference Counting in CPython
  5. Python Documentation: The Meaning of Type Hints
  6. Clang Documentation: Optimization Levels and Assembly Output
  7. C++ Working Draft: Observable Behavior and the Abstract Machine
  8. LLVM: Automatic Vectorization
  9. Python Documentation: time and monotonic_ns
  10. Python Documentation: Bytecode and LOAD_FAST
  11. PEP 659: Specializing Adaptive Interpreter
  12. CPython Documentation: JIT Compilation and Configuration
  13. CPython Documentation: Thread States and the GIL
  14. C++ Working Draft: Integer Ranges and Overflow Behavior
  15. NumPy: Executing Array Operations in Compiled Code
  16. NumPy: Numeric Types and Overflow
  17. Numba: Loops and Nopython Mode
  18. timeit Documentation: Repeated Measurements and Excluding Setup
  19. Python Documentation: assert and the -O Option
  20. C++ Working Draft: steady_clock
Based on the two programs and the reported timings: C++ 9 seconds; Python 514 seconds. This ratio is not a general performance guarantee for other programs.
Actual visitors 160,577
Visitors today 1,616
Total page views 1,852,389
Page views today 1,892
Book downloads 39,649