MicroPython Benchmarking

ตัวอย่างโค้ดสำหรับเปรียบเทียบความเร็วในการคำนวณด้วยไมโครไพธอน โดยใช้บอร์ดไมโครคอนโทรลเลอร์ที่แตกต่างกัน

กรณีในการทดสอบ

เพื่อสาธิตการทดสอบและเปรียบเทียบความเร็วหรือระยะเวลาในการทำงานของโค้ด จึงได้ออกแบบฟังก์ชันในภาษาไมโครไพธอนดังต่อไปนี้ และนำไปทดสอบกับบอร์ดไมโครคอนโทรลเลอร์ เช่น STM32F4 / L4 / F7 และ ESP32

  • test1: สร้างตารางหรือ list เพื่อเก็บตัวเลขแบบทศนิยม N จำนวน เช่น N=1024 โดยใช้ List Comprehension และคำนวณค่าตามฟังก์ชัน math.sin()

  • test2: สร้างตารางหรือลิสต์ เพื่อเก็บตัวเลขจำนวนเต็มขนาด 32 บิต และเป็นเลขสุ่ม (Pseudo-random integer numbers) ทั้งหมด N จำนวน เช่น N=1024 โดยใช้วิธี List Comprehension และเรียกใช้ฟังก์ชัน urandom.getrandbits(32) จากนั้นหาค่าต่ำสุดและสูงสุดในรายการตัวเลขนั้น และนำข้อมูลในลิสต์ มาเรียงลำดับโดยใช้คำสั่ง sorted()

  • test3: สร้างตารางหรือลิสต์ เพื่อเก็บเลขสุ่มแบบ 32 บิต หรือ 4 ไบต์ แล้วนำไปใส่เพิ่มลงในโครงสร้างข้อมูลแบบ bytearray ซึ่งเป็นไบต์บัฟเฟอร์ และทำซ้ำทั้งหมด N ครั้ง N=1024 (ขนาดทั้งหมด 4*N ไบต์) จากนั้นจึงนำไปคำนวณหาค่า SHA256 Secure Hash Value โดยใช้คำสั่ง uhashlib.sha256()

  • test4: ทำการเข้าและถอดรหัสข้อมูล (Data Encryption / Decryption) ตามลำดับ โดยใช้อัลกอริธึม AES และเลือกใช้งานในโหมด CBC ซึ่งจะต้องกำหนด IV (Initial Vector) ขนาด 128 บิต (16 ไบต์) และใช้ขนาดของคีย์ (Secret Key) 128 บิต หรือ 256 บิต เพื่อเข้ารหัสข้อมูลที่ได้สุ่มมา ทั้งหมด N ไบต์ เช่น N=512

การทดสอบในแต่ละกรณี ซึ่งเขียนอยู่ในรูปแบบของฟังก์ชัน มีอาร์กิวเมนต์เป็น N จะทำทั้งหมด 10 ครั้ง และมีการจับเวลาในการทำงานในแต่ละครั้ง แล้วบันทึกเก็บไว้ จากนั้นจึงมาคำนวณหาค่าเฉลี่ย ค่าต่ำสุด และค่าสูงสุด (Min. / Avg. / Max. Execution Time) นอกจากนั้น ยังมีการตรวจสอบและคำนวณปริมาณหน่วยความจำที่ใช้สำหรับฟังก์ชัน (Memory Usage)

ข้อสังเกต: ถ้าไมโครคอนโทรลเลอร์มีวงจรที่ทำหน้าที่เป็น True Number Random Generator (Hardware RNG) ไมโครไพธอนมีคำสั่ง uos.urandom() ให้ใช้งาน แต่ถ้าไม่มี ให้ใช้คำสั่งของโมดูลชื่อ urandom แต่เป็นการทำงานแบบ Pseudo-Random (Software)

โค้ดไมโครไพธอนสำหรับทดสอบ มีดังนี้

import sys
import gc
import math
import machine
import utime as time
import uhashlib as hashlib
import ubinascii as binascii
import urandom as random
import ucryptolib as cryptolib
import uos as os
import sys
  
if sys.platform.startswith('pyb'):
    import pyb
elif sys.platform == 'esp32':
    # possible CPU freq. for ESP32
    # 20MHz, 40MHz, 80Mhz, 160MHz or 240MHz
    machine.freq(240 * 1000000 )

try:
    from uos import urandom
    # uos.urandom() on ESP32 uses the hardware RNG.
    rand_bytes = lambda n: urandom( n )
except ImportError:
    # the urandom() modules uses a pseudo-RNG.
    rand_bytes = lambda n: bytes([random.getrandbits(8) for i in range(n)])

info = os.uname()
sys_info = (info[0],info[3],)
print( 'Micropython-{} {}'.format(*sys_info) )
freq = machine.freq()
if type(freq) == list or type(freq) == tuple:
    freq = freq[0]
print( 'CPU freq.   [MHz]:', int( freq//1000000 ) )
gc.collect()
print( 'Free mem. [bytes]: {}\n'.format(gc.mem_free()) )

###########################################################
def test1(N=1024):
    # generate a table of N sine values (N is the table size.)
    values = [math.sin(math.pi*i/N) for i in range(N)]
    del values

###########################################################
def test2(N=1024):
    # generate a table of 32-bit random values (N is the table size.)
    values = [random.getrandbits(32) for i in range(N)]
    # sort the values in the table
    sorted_values = sorted(values)
    assert(sorted_values[0]  == min(values),'error')
    assert(sorted_values[-1] == max(values),'error')
    del sorted_values, values

###########################################################
def test3(N=1024):
    # calculate SHA256 hash value from random numbers
    buf = bytearray()
    for i in range(N): # 4*N bytes
        x = random.getrandbits(32)  # 32-bit integer
        buf += x.to_bytes(4,'litte') # 4 bytes
    sha = hashlib.sha256()
    sha.update( buf )
    #print( binascii.hexlify(sha.digest()).decode() )
    del buf

###########################################################
def test4(N=256): # N is the plaintext size (in bytes)
    MODE_ECB, MODE_CBC, MODE_CTR = 1, 2, 6
    BLOCK_SIZE = 16 # block size (bytes) for CBC mode
    
    mode = MODE_CBC  # use the Cipher Block Chaining (CBC) mode
    # either 128-bit (16 bytes) or 256-bit key (32 bytes)
    key = bytes([random.getrandbits(8) for i in range(32)])
    # 128-bit or 16-byte initial vector (IV)
    iv = rand_bytes(BLOCK_SIZE)
    
    # random plaintext N bytes long
    plaintext = rand_bytes( N )
    plaintext_size = len(plaintext)
    if plaintext_size % 16 != 0:
        # padding with zeros
        plaintext += bytes((16 - plaintext_size % 16)*[0x00])
    #print( '>',binascii.hexlify(plaintext).decode() )

    # create an AES object for encryption
    aes = cryptolib.aes(key,MODE_CBC,iv)

    # perform encryption
    encrypted = aes.encrypt(plaintext)
    #print( '>', binascii.hexlify(encrypted).decode() )
    # create an AES object for decryption
    aes = cryptolib.aes(key,MODE_CBC,iv)
    decrypted = aes.decrypt(encrypted)
    #print( '>', binascii.hexlify(decrypted).decode() )
    assert(decrypted==plaintext)

###########################################################
# list of the tests 
tests  = [test1, test2, test3, test4]
# specify the argument N for the tests
N_args = [1024,1024,1024,512] 

NUM_TEST_RUNS = 10
for test in tests:
    values = []
    mem_used_max = 0
    for i,arg in zip(range(NUM_TEST_RUNS),N_args):
        gc.collect()
        mem_free = gc.mem_free()
        if sys.platform.startswith( 'pyb' ):
            start = pyb.micros()
            test(arg)
            exec_time = pyb.elapsed_micros(start)
        elif sys.platform == 'esp32':
            start = time.ticks_us()
            test()
            finish = time.ticks_us()
            exec_time = time.ticks_diff( finish, start )
        else:
            exec_time = 0
        
        values.append( exec_time )
        mem_used = (mem_free - gc.mem_free())
        mem_used_max = max(mem_used_max, mem_used)
        
    # calculate min/avg/max value of execution times
    min_exec = min(values)/1000
    avg_exec = sum(values)/len(values)/1000
    max_exec = max(values)/1000
    values = (min_exec, avg_exec, max_exec)
    print( test.__name__ ) # show the test name
    print('  Exec. time: {:.3f}/{:.3f}/{:.3f} msec'.format(*values) )
    print('  Mem. used : {} bytes used (max.)'.format( mem_used_max) )
    del values

ผลการทดสอบและเปรียบเทียบ

1) บอร์ด ESP32 4MB Flash (no SPIRAM), 240MHz CPU clock

ชิป ESP32 นั้น มีหน่วยความจำ SRAM 520 KB อยู่ภายใน แต่ใช้ SPI (Serial) Flash ภายนอก มีวงจรสำหรับช่วยคำนวณเกี่ยวกับความปลอดภัย (Crypto Hardware Acceleration) เช่น AES Accelerator, SHA Accelerator, RSA Accelerator และ True RNG เป็นต้น

ตัวอย่างข้อความเอาต์พุต มีดังนี้

Micropython-esp32 v1.13 on 2020-09-02 CPU freq. [MHz]: 240 Free mem. [bytes]: 103504 test1 Exec. time: 70.964/71.159/71.700 msec Mem. used : 53392 bytes used (max.) test2 Exec. time: 43.571/45.809/47.708 msec Mem. used : 33328 bytes used (max.) test3 Exec. time: 51.147/51.361/51.875 msec Mem. used : 62240 bytes used (max.) test4 Exec. time: 1.798/1.912/2.190 msec Mem. used : 1280 bytes used (max.)

2) บอร์ด WEACT STM32F411CEU6 Black Pill

ตัวประมวผลทำงานที่ความเร็ว 96 MHz สำหรับไมโครไพธอน ชิป STM32F411CE มีหน่วยความจำ 512 KB of Flash / 128 KB of SRAM อยู่ภายใน แต่ไม่มีวงจร True RNG

Micropython-pyboard v1.13-268-gf7aafc062-dirty on 2021-01-09 CPU freq. [MHz]: 96 Free mem. [bytes]: 78480 test1 Exec. time: 18.795/32.791/37.515 msec Mem. used : 53392 bytes used (max.) test2 Exec. time: 24.957/44.409/52.432 msec Mem. used : 33424 bytes used (max.) test3 Exec. time: 29.516/59.391/69.363 msec Mem. used : 61728 bytes used (max.) test4 Exec. time: 10.778/17.986/20.390 msec Mem. used : 8320 bytes used (max.)

3) บอร์ด PYBV3 STM32F405RGT6

ตัวประมวผลทำงานที่ความเร็ว 168 MHz สำหรับไมโครไพธอน และชิป STM32F405RGT6 มีหน่วยความจำ 1 MB of Flash / 192 KB of SRAM อยู่ภายใน และมีวงจร True RNG

Micropython-pyboard v1.13-268-gf7aafc062-dirty on 2021-01-09 CPU freq. [MHz]: 168 Free mem. [bytes]: 67440 test1 Exec. time: 13.525/23.590/26.946 msec Mem. used : 53392 bytes used (max.) test2 Exec. time: 15.694/28.543/34.498 msec Mem. used : 32816 bytes used (max.) test3 Exec. time: 19.509/38.632/45.161 msec Mem. used : 62272 bytes used (max.) test4 Exec. time: 2.367/3.671/4.106 msec Mem. used : 4064 bytes used (max.)

4) บอร์ด NUCLEO-STM32L476RG

ตัวประมวผลทำงานที่ความเร็ว 80 MHz สำหรับไมโครไพธอน และชิป STM32L476RG มีหน่วยความจำ 1 MB of Flash / 128 KB of SRAM อยู่ภายใน และมีวงจร True RNG

Micropython-pyboard v1.13-268-gf7aafc062-dirty on 2021-01-09 CPU freq. [MHz]: 80 Free mem. [bytes]: 51088 test1 Exec. time: 29.720/55.746/64.435 msec Mem. used : 26704 bytes used (max.) test2 Exec. time: 36.769/63.037/73.883 msec Mem. used : 33136 bytes used (max.) test3 Exec. time: 41.802/83.779/97.831 msec Mem. used : 30880 bytes used (max.) test4 Exec. time: 4.102/6.259/6.983 msec Mem. used : 4064 bytes used (max.)

5) บอร์ด NUCLEO-F767ZI

ตัวประมวผลทำงานที่ความเร็ว 216 MHz สำหรับไมโครไพธอน และชิป STM32F767ZI มีหน่วยความจำ 2 MB of Flash / 512 KB of SRAM อยู่ภายใน และมีวงจร True RNG

Micropython-pyboard v1.13-268-gf7aafc062-dirty on 2021-01-09 CPU freq. [MHz]: 216 Free mem. [bytes]: 265184 test1 Exec. time: 6.738/11.747/13.423 msec Mem. used : 53392 bytes used (max.) test2 Exec. time: 7.350/13.956/16.657 msec Mem. used : 32944 bytes used (max.) test3 Exec. time: 9.509/18.470/21.515 msec Mem. used : 61984 bytes used (max.) test4 Exec. time: 1.118/1.725/1.931 msec Mem. used : 4064 bytes used (max.)

จากผลการทดลอง เมื่อรันโค้ดทดสอบสำหรับไมโครไพธอน เราพอจะกล่าวได้ว่า

  • STM32F405RGT6 ทำงานได้เร็วกว่า STM32F411CEU6 และ STM32L476RG แต่ยังช้ากว่า STM32F767ZI

    • STM32F767ZI มีตัวประมวลผล ARM Cortex-M7 และใช้ความถี่ได้สูงกว่า STM32F4 ที่มีตัวประมวลผล ARM Cortex-M4F

  • STM32L476RG ทำงานได้ช้ากว่า STM32F411CEU6 และ STM32F405RGT6 นั้นก็เพราะว่า มีการใช้ความถี่ของ CPU clock ที่ต่ำกว่า (STM32L4 มี ARM Cprtex-M4F Core แต่ประหยัดพลังงานได้ดีกว่า STM32F4 จึงไม่เน้นการทำงานที่ความถี่สูง)

  • STM32F411CE ไม่มี True RNG จึงทำงานได้ช้ากว่า STM32F4 ที่มี True RNG เมื่อจำเป็นต้องสร้างข้อมูลแบบสุ่มตัวเลข

  • STM32F4 และ STM32L4 ทำงานได้เร็วกว่า ESP32 ยกเว้นกรณีที่ใช้ AES

    • อาจเป็นเพราะว่า ESP32 ต้องดึงข้อมูลคำสั่งจาก (external) Serial Flash แม้ว่าจะมี Cache อยู่ภายใน และทำงานด้วยความถี่สูงกว่า (240 MHz)

    • แต่ก็ทำงานได้ดีกว่า STM32F4 ถ้าต้องใช้ AES เนื่องจากมี AES Accelerator

Last updated