Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

MicroPython for Microcontrollers  Module 7 – Memory limits, speed, native and viper code

Measuring and improving speed

Time MicroPython code with ticks_us() and ticks_diff(), keep the fastest of several runs on a board, and speed up loops with const(), locals and integer maths.

  • Intermediate
  • 15 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: Ticks wrap, so never subtract them · Time the best of several runs, on the board · Check yourself. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Measure code with ticks_us() and ticks_diff() and report the fastest of several runs
  • Explain why timings taken on a computer or in a browser say nothing about a board
  • Work out how much a faster hot spot speeds up the whole program
  • Apply const(), local names and integer maths to a hot loop

Before you start

What you’ll learn · Before you start

What you will learn

  • Measure code with ticks_us() and ticks_diff() and report the fastest of several runs
  • Explain why timings taken on a computer or in a browser say nothing about a board
  • Work out how much a faster hot spot speeds up the whole program
  • Apply const(), local names and integer maths to a hot loop

Before you start

On this page

time.ticks_us() on a Pico counts up to 1,073,741,823 and then starts again at 0, so end - start comes out wrong, by about a billion, whenever a timing straddles the wrap, which comes round every 17.9 minutes. Timing on a board goes wrong in quieter ways too: one run instead of several, a run that includes a collection, or a number measured on a computer.

Below, code is timed the way that holds up on a board, with ticks_diff() and the best of several runs, and the changes that make a hot loop faster are named: const(), local names, integer maths and less work per sample.

Ticks wrap, so never subtract them

The ticks functions return a counter that wraps: ticks_us() in microseconds, ticks_ms() in milliseconds. The time module’s documentation promises only that the period is a power of two; on 32-bit ports such as rp2 and esp32 it is 2³⁰, every positive small integer: An integer that MicroPython keeps inside the object word itself, with no heap allocation: on most 32-bit ports one from -2**30 to just below 2**30. Larger integers stay exact but are stored on the heap. More in “Numbers, bytes and text on small chips”, so a reading never needs the heap. This program does the arithmetic the way extmod/modtime.c does it:

ticks_diff() and ticks_add() across the wrap Python · ticks/ticks_model.py
"""MicroPython's ticks arithmetic on a 32-bit port, written as extmod/modtime.c does it."""
PERIOD = 2 ** 30  # MICROPY_PY_TIME_TICKS_PERIOD: every positive small integer
HALF = PERIOD // 2


def ticks_add(ticks, delta):
    if not -HALF < delta < HALF:
        raise OverflowError("ticks interval overflow")
    return (ticks + delta) & (PERIOD - 1)


def ticks_diff(end, start):
    return ((end - start + HALF) & (PERIOD - 1)) - HALF


start = ticks_add(0, -200)  # 200 us before ticks_us() wraps to 0
end = ticks_add(start, 711)  # 711 us later, after the wrap
print("start %d, end %d" % (start, end))
print("end - start            %d" % (end - start))
print("ticks_diff(end, start) %d" % ticks_diff(end, start))
print()
for gap in (HALF - 1, HALF):
    later = (start + gap) & (PERIOD - 1)
    print("%d ticks later: ticks_diff gives %d" % (gap, ticks_diff(later, start)))
try:
    ticks_add(start, HALF)
except OverflowError as e:
    print("ticks_add(start, %d): OverflowError: %s" % (HALF, e))
print()
print("ticks_us() wraps every %d us, %.1f minutes" % (PERIOD, PERIOD / 60e6))
print("ticks_ms() wraps every %d ms, %.1f days" % (PERIOD, PERIOD / 86400e3))

Output

start 1073741624, end 511
end - start            -1073741113
ticks_diff(end, start) 711

536870911 ticks later: ticks_diff gives 536870911
536870912 ticks later: ticks_diff gives -536870912
ticks_add(start, 536870912): OverflowError: ticks interval overflow

ticks_us() wraps every 1073741824 us, 17.9 minutes
ticks_ms() wraps every 1073741824 ms, 12.4 days

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ticks_model.py

The timing started 200 µs before the wrap and lasted 711 µs. Plain subtraction gives −1,073,741,113, while ticks_diff() gives 711, because it works modulo the period of the ticks: The counts that time.ticks_ms(), ticks_us() and ticks_cpu() return: they wrap to 0 at a power of two, 2**30 on 32-bit ports, so two readings are compared only with time.ticks_diff(). More in “Pins: outputs, inputs, pull-ups and Signal”. It stays right while the two readings are less than half a period apart: 8.9 minutes for ticks_us(), 6.2 days for ticks_ms(). At half a period its answer turns negative, and ticks_add() refuses such an interval with OverflowError.

Comparing ticks with < or >

deadline = time.ticks_ms() + 200 with if time.ticks_ms() > deadline: works until the counter nears its wrap. A deadline made 100 ms before the wrap lies beyond the largest value the counter reaches, so the test never comes true. Make it with time.ticks_add(time.ticks_ms(), 200) and test time.ticks_diff(deadline, time.ticks_ms()) <= 0.

Checkpoint

Check: Ticks that wrap, and ticks_diff()

  1. Question 1 of 2 On a Pico, start = 1073741000 and end = 300, both from ticks_us(). What does ticks_diff(end, start) return?

    Type a number.

    A hint

    The counter wraps at 2³⁰ = 1,073,741,824.

    Show the answer: Question 1 of 2

    Answer: 1124 µs

    From 1,073,741,000 to the wrap at 1,073,741,824 is 824 µs, and from 0 to 300 another 300 µs: 1,124 µs. Plain subtraction would give −1,073,740,700.

  2. Question 2 of 2 Which test of a deadline made with deadline = ticks_add(ticks_ms(), 200) keeps working across the wrap?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ticks_diff(deadline, ticks_ms()) <= 0

    Ticks may be compared only through ticks_diff(). ticks_diff(deadline, now) is the time left: positive before the deadline, zero or negative once it has passed, whatever the counter's position.

Time the best of several runs, on the board

Measuring speed starts with distrust of a single run: a collection, an interrupt or a slow first call can land in it. MicroPython’s speed guide times functions with ticks_us() and ticks_diff(); repeat the call, call gc.collect() before each run so that no garbage from earlier is collected inside it, and keep the best run as the code’s own cost and the slowest as a warning. timing.py does that, and bench.py prints what ran it beside the numbers:

The best and the slowest of seven runs, with what ran them

Python · bench/bench.py

"""Time a checksum the way you would on a board, and print what ran it beside the numbers."""
import sys

from timing import best_of


def checksum(data):
    total = 0
    for b in data:
        total = (total + b) & 0xFFFF
    return total


impl = sys.implementation
print("%s %d.%d.%d on %s" % ((impl.name,) + tuple(impl.version[:3]) + (sys.platform,)))
fast, slow = best_of(checksum, bytes(range(256)) * 8)
print("checksum of 2,048 bytes: best %d us, slowest %d us of 7 runs" % (fast, slow))

Output

cpython 3.14.8 on darwin
checksum of 2,048 bytes: best 67 us, slowest 76 us of 7 runs

This output changes from run to run: the times are measured afresh on every run

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bench.py

bench/timing.py

"""best_of(): time a function as you would on a board, the best of several runs.

On MicroPython it uses ticks_us() and ticks_diff(). On CPython, which has no ticks, a
microsecond counter made from perf_counter_ns() stands in; it never wraps.
"""
import gc

try:
    from time import ticks_diff, ticks_us
except ImportError:
    from time import perf_counter_ns

    def ticks_us():
        return perf_counter_ns() // 1000

    def ticks_diff(end, start):
        return end - start


def best_of(func, arg, runs=7):
    """The fastest and the slowest of several runs, in microseconds."""
    times = []
    for _ in range(runs):
        gc.collect()  # start each run with no garbage left to collect
        start = ticks_us()
        func(arg)
        times.append(ticks_diff(ticks_us(), start))
    return min(times), max(times)

The recorded run is CPython 3.14.8 on a computer, and Run in this page runs Pyodide, CPython compiled for the browser: two different numbers, and a board gives a third. MicroPython’s interpreter is not CPython, and a Pico, which the rp2 port runs at 125 MHz, is not a laptop, so only the board’s own run says how fast the code is there.

Time it on a board

Copy timing.py and bench.py to a Pico with mpremote cp timing.py : and run mpremote run bench.py. The first line now names micropython and rp2, and the times are the board’s.

Measuring on the board also says where to start improving speed. Once it shows the code is too slow, these changes make a hot loop faster, roughly in the order to try them:

  • Less work per sample. The speed guide puts the algorithm first: a moving average that adds up its window again for every sample does far more work than one that keeps a running total.
  • Integer maths. On rp2 and esp32 every float result is a new heap object, and some ports compute floats in software. Whole units, such as millivolts instead of volts, keep the loop on small integers.
  • Local names. A local is read from a slot of the function’s frame, while a global or an attribute such as self.buf is looked up by name. Copying it into a local before the loop, buf = self.buf, saves that lookup.
  • const(). const(): MicroPython's declaration NAME = const(expr): the compiler works out expr and writes its value into the bytecode wherever NAME is used. A name that starts with an underscore is not kept in the module's globals at all. makes the compiler write 500 into the bytecode wherever _LIMIT appears, so no lookup happens at all. With the leading underscore, the name is not even kept in the module’s globals.
  • Machine code. The native and viper emitters compile a function to machine code: the next lesson.

How much improving speed in one place buys the whole program depends on the share of its time that hot spot: The part of a program where it spends most of its running time, found by measuring. Speeding it up shortens the whole run only by the time it takes away, so its share of the time decides what a change can win. takes.

Checkpoint

Check: The best of several runs, on the board

  1. Question 1 of 2 Why does timing.py call gc.collect() before each run instead of once at the start?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: So that no collection of garbage left by an earlier run happens inside the timed region

    Each run leaves garbage, and the next request for memory might collect it. Collecting before every run moves that pause out of the timed region, so the best run measures the code itself.

  2. Question 2 of 2 The recorded run of bench.py and a press of Run in this page give two different times for the same checksum. What do the two numbers say about a Pico?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing; only a run on the Pico says how fast it is there

    A timing belongs to the interpreter and the machine that produced it. Time the code on the board itself with the same harness.

Check yourself

Ticks and timing on the board, one question at a time; a wrong choice says what it misses.

Practice that adapts to you

Practise this lesson

6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 After how many microseconds does ticks_us() on a 32-bit port such as rp2 start again at 0?

    Type a number.

    Show the answer: Question 1 of 6

    Answer: 1073741824 µs

    The period is 2³⁰ = 1,073,741,824 µs, 17.9 minutes: every positive small integer on a 32-bit port.

  2. Question 2 of 6 A loop times a step with ticks_us() and the step sometimes takes 10 minutes. What does ticks_diff() report then?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: A wrong, negative number, because 10 minutes is more than half the period

    ticks_diff() returns a value from −2²⁹ to 2²⁹ − 1. A real interval of 10 minutes, 600,000,000 µs, is beyond 2²⁹ = 536,870,912, so it wraps to a negative number. Use ticks_ms() for such steps.

  3. Question 3 of 6 A scheduler calls ticks_add(ticks_ms(), delay_ms) with a delay of 7 days. What happens on a Pico?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: It raises OverflowError: ticks interval overflow

    ticks_add() accepts only intervals that ticks_diff() can measure afterwards, under half the period: 6.2 days for ticks_ms(). A 7-day delay raises OverflowError, so long schedules need their own count of days.

  4. Question 4 of 6 Which number of seven timed runs estimates the code's own cost best?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: The best, the shortest run

    Collections, interrupts and other work can only make a run longer. The shortest run is the closest to the code's own cost; the slowest shows how bad the noise gets.

  5. Question 5 of 6 Seven runs of a function on a Pico take 410, 395, 397, 396, 395, 398 and 396 µs. What does the harness report as the cost, and what is the 410 µs run?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: 395 µs is the cost, and 410 µs is a warning that something, such as an interrupt or a slow first call, landed in one run

    The best of several runs, 395 µs, is the closest to what the code itself costs, because a collection, an interrupt or a slow first call can only add time. The slowest, 410 µs, is kept as a warning of what can land in a run.

  6. Question 6 of 6 A function takes 40 µs on a laptop, and a 10 kHz control loop on a Pico has 100 µs per pass. What is the right next step?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: Time the function on the Pico with the same harness before deciding anything

    Only a run on the board says how long the code takes there. Use the harness on the Pico, and keep the best of several runs.

Benchmarks that hold up: the clock's limits and the hot spot's share, worked out Pro: opens with a Pro, Premium or Ultimate pass

A measurement on a board is only as good as its clock and its setup, and a speed-up is only worth what it adds to the whole program. This part works out both with numbers, before any code is changed.

In this part:

  • How long each counter can measure, and what spoils a run
  • Before you change code: what the whole program gains, worked out

1 diagram · 1 runnable example (Python) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Hot-loop rewrites: const(), local names and less work per sample, checked Pro: opens with a Pro, Premium or Ultimate pass

The changes that make a hot loop faster each come with rules that decide whether they work. Below, const() is audited line by line, and a moving average is rewritten to do a constant amount of work per sample.

In this part:

  • What const() folds, and what it saves in RAM
  • Less work per sample: a running total instead of a sum
  • Exercise: a moving-average alarm with less work per sample
  • Make a moving-average alarm do less work per sample
    • Results of the sample tests

2 runnable examples (Python, driver.py) · 1 exercise with sample tests · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

Warm-up (fresher): Why use time.ticks_diff() instead of subtracting two ticks_us() values?

ticks_us() wraps: on 32-bit ports it counts to 2³⁰ − 1 and starts again at 0. A plain end - start across the wrap is wrong by about a billion. ticks_diff(end, start) works modulo the period and gives the right signed difference as long as the two readings are less than half a period apart, about 8.9 minutes for microseconds.

  1. Fresher to mid

    A colleague timed a filter function at 40 µs on their laptop and planned a 10 kHz control loop on a Pico around that number. What is wrong with the plan, and how would you measure the function properly?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A Pico's 1 kHz loop needs 1,200 µs per sample, and the budget is 1,000. Walk through how you would make it fit, in order, and say what you would measure at each step.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A driver module defines 60 register addresses as REG_NAME = const(0x..). A teammate says the constants waste RAM. Is that right, how much is it, and what do you change without breaking the modules that use the driver?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How often does ticks_us() wrap on a Pico, and what does end - start give across the wrap?

    Every 2³⁰ µs, about 17.9 minutes. Plain subtraction is off by about a billion; ticks_diff(end, start) gives the right answer.

  2. What does ticks_add() do with an interval of half a period or more?

    It refuses it with OverflowError. ticks_diff() is right only while two readings are less than half a period apart, 8.9 minutes for ticks_us() and 6.2 days for ticks_ms(); at half a period its answer turns negative.

  3. How should a deadline 200 ms ahead be made and tested?

    deadline = time.ticks_add(time.ticks_ms(), 200), then time.ticks_diff(deadline, time.ticks_ms()) <= 0. A deadline made with + can lie beyond the largest value the counter reaches.

  4. How do you time a function so the number holds up on a board?

    Run it several times with gc.collect() before each run, time each run with ticks_us() and ticks_diff(), and report the fastest as the code's cost; the slowest shows what can get in its way.

  5. Why does a timing taken on a laptop or in the browser say nothing about a Pico?

    MicroPython's interpreter is not CPython, and a Pico at 125 MHz is not a laptop: only the board's own run measures the board.

  6. In what order do you try the changes that speed up a hot loop?

    Less work per sample first, then integer maths, local names such as buf = self.buf before the loop, and const(); then native or viper machine code.

Key takeaways

  • Ticks wrap at 2³⁰ on 32-bit ports, so subtract them only with ticks_diff() and add to them only with ticks_add().
  • Time a function several times on the board itself, with gc.collect() before each run, and keep the best run; numbers from a computer or a browser say nothing about the board.
  • Make a hot loop faster by doing less work per sample first, then integer maths, local names and const().

Terms in this lesson

const()
MicroPython's declaration NAME = const(expr): the compiler works out expr and writes its value into the bytecode wherever NAME is used. A name that starts with an underscore is not kept in the module's globals at all.
hot spot
The part of a program where it spends most of its running time, found by measuring. Speeding it up shortens the whole run only by the time it takes away, so its share of the time decides what a change can win.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.