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.
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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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()
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:
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) Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.bufis 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
_LIMITappears, 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
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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
-
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?
-
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.
-
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?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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 withticks_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
- time – time related functions (ticks_ms, ticks_us, ticks_diff, ticks_add) (MicroPython project)
- Maximising MicroPython speed (identifying the slowest section, const, caching references) (MicroPython project)
- micropython – const() (MicroPython project)
- MicroPython on microcontrollers (constants and RAM) (MicroPython project)
- extmod/modtime.c, ticks_diff() and ticks_add() (v1.29.0) (MicroPython project)
- py/mpconfig.h, MICROPY_PY_TIME_TICKS_PERIOD (v1.29.0) (MicroPython project)
- MicroPython v1.26.0 release notes (float constants folded) (MicroPython project)
- MicroPython v1.29.0 release notes (const assignments with type hints) (MicroPython project)
- Quick reference for the RP2 (the default CPU frequency) (MicroPython project)
- time: perf_counter_ns() (Python Software Foundation)
- Validity of the single processor approach to achieving large scale computing capabilities (Amdahl) (ACM)
Report a problem with this lesson
Kept only in this browser. Your Learn progress