Mittwoch, 8. Juni 2022

Python und Geschwindigkeit auf verschiedenen Intel und AMD CPUs - und einem Apple Bionic Chip

Vor ein paar Jahren gab es hier im Blog einen Post, der die Geschwindigkeit eines Pythonprogramms zum Number-Crunching (in Form eines Primzahlentests) beschriebt (Link zum Artikel).

Ich hatte jetzt kürzliche die Gelegenheit, die Ausführungsgeschwindigkeit auf vier verschiedenen Mobil CPUs verschiedener Hersteller und Generation zu vergleichen, nämlich:

Die Prozessoren sind also eine zum Zeitpunkt des Schreibens des Artikels eine ca. 7 Jahre alte CPU (der Core i5-5200U, erschienen 2015), ein Core i7 Prozessor der 10. Generation (erschienen zweite Jahreshälfte 2019), ein Core i7 Prozessor der 11. Generation (erschienen 2. Jahreshälfte 2021)  sowie die ebenfalls 2021 erschienene Ryzen 7 5700U CPU.

Außerdem habe ich noch einen Test auf einem iPad Air gemacht. Mehr dazu am Ende des Artikels.

Verglichen habe ich nur die Ausführungsgeschwindigkeit des Python-Programms ohne Optimierungen / Beschleunigungen, also ohne PyPy, Cython, JIT Compiler etc.

Als Betriebssystem diente für den Core i5-5200U und den AMD Ryzen 7 5700U Ubuntu 22.04 mit Python 3.10.4 aus den offiziellen Ubuntu Quelle und für die beiden Intel Core i7 Windows 11, ebenfalls mit Python 3.10.4 aus dem Windows App Store, also die Python-Version, die direkt von der PSF bereit gestellt wird.
Das ganze wurde immer bei ruhendem Desktop gemessen, d.h. außer dem Terminal, in dem das Python-Programm läuft, läuft kein weiteres Programm. Es wurde jedes Skript 3x ausgeführt, die weiter unten auf geführten Zeitangabe sind der Mittelwert der Ausführungszeiten.

Als erste habe ich die Ausführungsgeschwindigkeit mit nur einem Prozess gemessen, das Programm sieht wie folgt aus:

import math
from time import time

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419,
    777777722155555333,
    777777722155555335,
    9999999900000001,
    2327074306453592351,
    2327074306453592353]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    for number in PRIMES:
        prime = is_prime(number)
        print('{} is prime: {}'.format(number, prime))

if __name__ == '__main__':
    start = time()
    main()
    end = time()
    print(f'Time: {end - start}')

Die Ergebnisse sind wie folgt:

  • Core i5-5200U: 204 Sekunden
  • Core i7-10510U: 111 Sekunden
  • Core i7-1165G7: 68 Sekunden
  • Ryzen 7 5700U: 78 Sekunden

Wie zu erwarten war, sind die neueren CPUs deutlich schneller. Interessant ist auch die relative große Unterschied zwischen dem Core i7 10. Generation und 11. Generation, letzterer ist etwas 60% schneller. Hier hat Intel deutlich in Sachen Leistung nachgelegt, zumindest was die Performance von nur einem Kern angeht.
Zweite Beobachtung: im weiter oben verlinkten Blogpost wurde der gleiche Code ebenfalls auf dem gleichen Core i5-5200U ausgeführt, nur damals unter Python 3.6. Die Geschwindigkeit war damals quasi identisch mit der hier gemessenen. Auch wenn Python (bzw. genau genommen die Referenzimplementierung CPython) einige Verbesserungen in Sachen Geschwindigkeit bekommen hat, spielen diese zumindest für dieses Szenario keine Rolle, der Code ist auf der gleichen CPU bzw. dem gleichen Rechner wie damals quasi gleich schnell.

Im zweiten Test wurde das Programm modifiziert, so dass mit Hilfe des concurent.futures Moduls mehrere Prozesses zum Rechnen gestartet. Da die Core i5 5200U CPU und auch die neueren Core i7 CPUs "nur" vier Kerne haben, wurde als Prozessanzahl vier gewählt, damit im Idealfall jeder Prozess auf einem Kern läuft.

Der Code sieht wie folgt aus:

import math
from time import time
import concurrent.futures

WORKERS = 4

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419,
    777777722155555333,
    777777722155555335,
    9999999900000001,
    2327074306453592351,
    2327074306453592353]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor(max_workers=WORKERS) as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))


if __name__ == '__main__':
    print(f'using {WORKERS} workers for calculation')
    start = time()
    main()
    end = time()
    print(f'Time: {end - start}')

Die Ergebnisse für den Multiprozessansatz sind:

  • Core i5-5200U: 111 Sekunden
  • Core i7-10510U: 89 Sekunden
  • Core i7-1165G7: 50 Sekunden
  • Ryzen 7 5700U: 51 Sekunden

Auch hier ist der Leistungsunterschied zwischen dem Core i7 10. und 11. Generation erheblich, der neuere ist ca. 80% schneller. Der Core i7-1168G7 und der Ryzen 7 5700U sind quasi gleich schnell.

Ein zusätzlicher Test mit dem Ryzen 7 und acht Workern brachte keine weitere Beschleunigung, das Ergebnis war quasi identisch mit dem mit vier Workern. Dies liegt im gegebenen Test aber ziemlich sicher daran, dass die ersten sechs Prüfungen relativ schnell abgeschlossen sind, d.h. man zieht für die restlichen fünf Zahlen keinen echten Nutzen aus mehr Kernen, weil nicht mehr so viele Prozesse laufen.

Festzuhalten bleibt aber auch: natürlich sind neuere Prozessoren schneller, aber bringen (bei weitem) nicht so viel wie der Einsatz von schnelleren Python-Implementierungen und JIT Compilern.
Alleine der simple Einsatz von PyPy statt CPython auf dem "alten" Core i5 Prozessor führt den Code doppelt so schnell aus wie die Core i7 CPU der 11. Generation oder der Ryzen 7 5700U.

Zusätzlich zu den diversen Intel und der AMD CPU habe ich den nicht-parallelen Code auf einem iPad Air 3. Generation, iOS 15.5 und Python 3.6.1 aus der Pythonista ausgeführt. Dieses iPad hat eine A12 Bionic CPU. Der parallelisierte Code funktioniert ja leider nicht unter iOS, weil, zumindest zum Zeitpunkt des Schreibens des Artikels, ein Prozess keine weiteren Prozesse starten darf.

Der A12 Bionic braucht für den obigen Code ca. 104 Sekunden - und ist damit ca. 10% schneller der getestet Intel Core i7 der 10. Generation. Schon ganz ordentlich für einen Prozessor, der ca. ein Jahr vor dem Intel Prozessor erschienen ist, wobei allerdings auch der Takt der leistungsstärkeren Kerne des A13 mit 2,,49 GHz (deutlich) höher ist als die 1,80 GHz Core i7-10510U.