pythonのリアルタイム性能

pythonで`sleep(遅延)`したときに、実際にどれ位の時間スリープするかという話です。マイクロ秒単位のリアルタイム処理が可能みたいです。ここで「リアルタイム性」とは、狙った時刻に処理を行えるという性質です。OSに依存する話で、以下の話はラズパイ4B上の[リアルタイムパッチが当てられたDebian 11 BullseyeのLinuxカーネル](https://packages.debian.org/bullseye/linux-image-rt-arm64)と[普通のカーネル](https://packages.debian.org/bullseye/linux-image-arm64)実験しました。x86_64でも同じことをやって傾向は同じでした。

**更新: qemu内でも同じことを行って同じ傾向を得たので追記しました**

# Linux上のプログラムの実行優先度の変更
## [nice](https://qiita.com/hana_shin/items/438dfe93a765ffa337ae)
ナイス値(-20 〜 +19)を変化させます。ナイス値が小さいほうが優先して実行されるます。最も優先して実行するには `nice --20 やりたいこと` のようにプログラムを起動します。

## [chrt](https://qiita.com/hana_shin/items/7bdfd485b322cf9ad521)
ナイス値の調整よりもさらに優先度を上げたい場合や下げたい使います。割り込み処理も含めてあらゆる処理を押しのけて実行させたい場合 `chrt --fifo 99 やりたいこと` とし、他に何もやることが無い場合にだけ実行させたい場合 `chrt --idle 0 やりたいこと` とします。

# 実験
## 実験の下準備
1. `apt-get install` [stress-ng](https://packages.debian.org/ja/bullseye/stress-ng) [linux-cpupower](https://packages.debian.org/ja/bullseye/linux-cpupower)
1. `cpupower frequency-set -g performance` でCPU動作周波数を最高値に固定します
1. `stress-ng --timeout 300 --parallel 0 --class pipe &` でOSとすべてのCPUに負荷を掛けます。`pipe` を `interrupt` に変えるとよりキビシクなります。
 
## 実験結果
狙った遅延と平均の誤差をマイクロ秒単位で表示しています。RTと標準とQEMUはそれぞれLinuxのカーネルの種類を表します。QEMUはQEMU内のDebian 11標準amd64カーネルです。QEMUを動かしているときにホストOSはほぼアイドル状態でした。`nice -19` および `chrt --idle 0` で起動したら遅延の計測プログラムが終了しなかったので、結果を載せていません。

|実行優先度・OS種別＼狙った遅延|100|1000|10000|100000|1000000|10000000|
|:-|-:|-:|-:|-:|-:|-:|
|普通に起動・標準|47403.7|1216.6|14996.0|164.6|1061.6|8126.5|
|普通に起動・RT|79.9|69.1|85.5|223.6|1082.1|10211.5|
|普通に起動・QEMU|15575.3|4335.5|134.1|9424.7|1068.2|9318.9|
|`nice --20`・標準|2404.2|85.8|32285.5|162.5|982.1|7887.1|
|`nice --20`・RT|132.0|89.1|120.8|174.0|3862.4|10071.6|
|`nice --20`・QEMU|3532.0|1384.8|5940.3|174.6|911.7|7940.7|
|`chrt --fifo 99`・標準|30.6|38.5|64.3|61.8|69.7|63.1|
|`chrt --fifo 99`・RT|31.6|21.0|44.6|56.1|64.7|66.4|
|`chrt --fifo 99`・QEMU|166.9|58.4|58.4|69.4|78.3|73.7|



リアルタイム性が必要な場合は `chrt --fifo 99` で起動するのがよいです。リアルタイムパッチを当てなくても `chrt --fifo 99` でかなりのリアルタイム応答性が実現できるようです。
上記のQEMU内での実験時には、ホストLinuxの負荷はほぼゼロでしたが、ホストLinuxの負荷が高い場合にはQEMUの実行が優先されるように`chrt --fifo`でQEMUが実行されるようにしないとQEMU内でのリアルタイム性能は保証されないはずです。`libvirt` や `virt-manager` から起動するQEMUの実行優先度の設定は[`vcpusched`, `iothreadsched` ならびに `emulatorsched`](https://libvirt.org/formatdomain.html#cpu-tuning) で行うことができます。

## 実験用プロググラム
以下のプログラムを用いて狙った遅延と実際の遅延の差（誤差）をマイクロ秒単位で測っています。`time_ns()` がPython 3.7で導入されたため、Python 3.7かそれ以降ではないと動作しません。使用したPythonバージョンはDebian 11標準の3.9です。

```python
import time

for 狙った遅延 in (0.0001, 0.001, 0.01, 0.1, 1.0, 10.0):
  print('狙った遅延=' + str(狙った遅延))
  誤差の合計 = 0
  for i in range(3):
    前の時刻 = time.time_ns()
    time.sleep(狙った遅延)
    後の時刻 = time.time_ns()
    誤差 = (後の時刻 - 前の時刻) / 1000000000 - 狙った遅延
    print('誤差={:.1f}マイクロ秒'.format(誤差*1000000))
    誤差の合計 += 誤差
  print('平均の誤差={:.1f}マイクロ秒'.format(1000000 * 誤差の合計 / 3))
```

上記のpythonプログラムを以下のシェルスクリプトで起動しました

```shell
#!/bin/sh

export LANG=C.UTF-8
exec </dev/null >>log.txt 2>&1
set -ex
date
uname -a
cpupower frequency-set -g performance
# 次の行で pipe を interrupt に変えるとよりキビシクなります
stress-ng --timeout 300 --parallel 0 --class pipe &
python3 sleep-test.py
nice --20 python3 sleep-test.py
chrt --fifo 99 nice --20 python3 sleep-test.py
nice -19 python3 sleep-test.py
#chrt --idle 0 nice -19 python3 sleep-test.py
kill -TERM $!
sleep 3
chrt --fifo 99 pkill stress
cpupower frequency-set -g schedutil
```