MicroPython _thread exercise

The purpose of this exercise is to test the behavior of MicroPython _thread, specifically for Raspberry Pi Pico series, whether _thread.start_new_thread() can provide real multi-core parallel speedup for pure Python CPU-bound work. 

mpy_thread_smp_bench.py
# mpy_thread_smp_bench.py
#
# Purpose:
#   Simple MicroPython benchmark to test whether _thread.start_new_thread()
#   can provide real multi-core parallel speedup for pure Python CPU-bound work.
#
# Method:
#   1. Run a CPU-bound loop in the main thread and measure the duration.
#      This is the single-thread baseline.
#
#   2. Run the same CPU-bound loop in two threads:
#       - one in the main thread
#       - one in a newly created _thread
#
#   3. Measure:
#       - duration of thread 0
#       - duration of thread 1
#       - total wall-clock time from starting both threads until both finish
#
# Interpretation:
#   Let single-thread duration be T.
#
#   If the two CPU jobs run fully in parallel on two cores:
#       total wall time ~= T
#       speedup indicator ~= 200
#
#   If the two jobs are serialized, for example because:
#       - the chip has only one core
#       - the OS/scheduler uses only one core
#       - MicroPython uses a GIL / global VM lock
#       - there is severe shared-resource contention
#   then:
#       total wall time ~= 2*T
#       speedup indicator ~= 100
#
# Notes:
#   - Choose N so that the single-thread run takes about 100 ms to 500 ms.
#     If N is too small, thread startup, printing, interrupts and scheduler
#     overhead will dominate the result.
#
#   - This benchmark intentionally avoids printing during the timed loop.
#     Results are stored and printed only after the measurement is complete.
#
#   - The lock is only used to protect the result flags/variables after the
#     timed work is finished. It is not part of the measured CPU loop.
#
#   - Different MicroPython ports behave differently. On some ports, threads
#     may be true parallel workers on separate cores. On others, Python
#     bytecode execution may be serialized even on dual-core hardware.

import os
import sys
import _thread
import time

# Adjust this value so that the single-thread run takes about 100-500 ms.
# If it is too fast, increase N. If it is too slow, decrease N.
N = 50000

lock = _thread.allocate_lock()
res = [0, 0]
done = [False, False]

def burn(n):
    """
    Simple CPU-bound loop.
    It does not intentionally allocate large objects or perform I/O.
    """
    x = 0
    for i in range(n):
        x += 1
    return x

def worker(idx):
    """
    Worker function executed by each thread.
    Measures only the CPU-bound loop, then stores the result.
    """
    t0 = time.ticks_us()
    burn(N)
    dt = time.ticks_diff(time.ticks_us(), t0)

    lock.acquire()
    res[idx] = dt
    done[idx] = True
    lock.release()

print("=========================================================")
print(sys.implementation[0], os.uname()[3],
      "\nrun on", os.uname()[4])
print("=========================================================")

# ---------- Single-thread baseline ----------
t0 = time.ticks_us()
burn(N)
single = time.ticks_diff(time.ticks_us(), t0)

print("single thread duration =", single, "us")

# ---------- Two-thread test ----------
done[0] = False
done[1] = False

t0 = time.ticks_us()

_thread.start_new_thread(worker, (1,))
worker(0)

# Wait until both workers have finished.
while True:
    lock.acquire()
    all_done = done[0] and done[1]
    lock.release()

    if all_done:
        break

    time.sleep_ms(1)

wall = time.ticks_diff(time.ticks_us(), t0)

print("thread 0 duration =", res[0], "us")
print("thread 1 duration =", res[1], "us")
print("total wall time   =", wall, "us")

# Speedup indicator:
#   100 means no throughput gain: wall ~= 2*single
#   200 means ideal dual-core gain: wall ~= single
print("speedup indicator (100 = serial/single, 200 = ideal dual):",
      (200 * single) // wall)



Comments

Popular posts from this blog

Python/PyQt6 slideshow run on Windows 11

480x320 TFT/ILI9488 SPI wih EP32C3 (arduino-esp32) using Arduino_GFX Library