Dat Python niet dezelfde ruwe prestaties kan neerzetten als Java of C is inmiddels lang en breed bekend. Maar in veel gevallen hoeft dat niet per se zo te zijn. Goed geoptimaliseerde Python-applicaties kunnen verrassend snel draaien. Misschien niet zo snel als Java of C, maar snel genoeg voor webapplicaties, data-analyse, management en automatiseringstools en andere taken. Je zou bijna vergeten dat je applicatie-prestatie inruilt voor ontwikkelaarsproductiviteit.

Het optimaliseren van Python prestaties hangt niet noodzakelijkerwijs af van één factor. Het gaat meer om het toepassen van alle beschikbare best practices en keuzes maken die het best passen bij het scenario waar jij op dit moment mee bezig bent. De mensen van Dropbox hebben een paar prachtige voorbeelden van Python optimalisaties.

In dit artikel zal ik de meest voorkomende Python-optimalisaties uitleggen. Sommige zijn drop-in oplossingen die iets meer vergen dan het vervangen van een item voor een ander (zoals het wijzigen van de Python-interpreter), maar de optimalisaties die het meest opleveren zullen ook meer werk vereisen.

1. Meten, meten, meten

Meten is weten en dat geldt ook Python. Begin met profiling door gebruik te maken van Pythons cProfile module en pak de line-level profiler erbij als je preciezer te werk wil gaan. De inzichten die je hebt opgedaan door een standaard function-level inspectie uit te voeren op een applicatie levert meer dan genoeg nuttige informatie op. (Je zou eventueel ook nog profieldata voor een enkele functie kunnen ophalen door de profilehooks module te gebruiken.)

Waarom een bepaald gedeelte van een app zo traag is en wat je eraan kan doen zal wat meer uitzoekwerk kosten. Het idee is om de focus te verkleinen en harde, bruikbare data te genereren. Test de boel met verschillende gebruiks- en deployscenario's. Ga niet vroegtijdig optimaliseren. Met gokken kom je niet ver.

Op de volgende pagina: Veelgebruikte data en wiskunde

2. Memoize (Cache) veelgebruikte data

Doe nooit keer op keer hetzelfde werk als je het ook één keer kunt doen en de resultaten kan opslaan. Als je een veelgebruikte functie hebt die voorspelbare resultaten genereert, kan je gebruik maken van de Python's opties om de resultaten te cachen in het geheugen. Opvolgende calls die hetzelfde resultaat genereren zullen vrijwel direct beschikbaar zijn.

Er zijn veel voorbeelden te vinden op het net waar wordt gedemonstreerd hoe dit werkt. Mijn favoriete memoization voorbeeld is zo minimaal als maar kan. Een van Pythons native libraries, functools, heeft de @functools.lru_cache decorator. Deze cacht de n recentste calls naar een functie. Dit is handig wanneer de waarde, die je cachet, verandert maar is relatief statisch binnen een bepaalde tijdsframe. Een lijst van de meestgebruikte items over de gehele dag is een goed voorbeeld.

3. Verhuis je wiskunde naar NumPy

Als je bezig bent met matrix- of array-gebaseerde wiskunde en je wil niet dat de Python-interpreter in de weg zit, gebruik NumPy. NumPy leunt op C-libraries voor het zware werk en biedt veel snellere array verwerking dan native Python's ingebouwde datastructuren.

Ook minder exotische wiskunde kan behoorlijk worden versneld door NumPy. Het pakket levert vervangers voor veel standaard wiskundige Python-operaties als min en max die veel sneller werken dan de Python originelen.

Een ander groot voordeel van NumPy is het efficiëntere geheugengebruik voor grotere objecten zoals lijsten met miljoenen items. Gemiddeld nemen grote objecten in NumPy ongeveer een vierde van het geheugen in beslag in tegenstelling tot de conventionele Python-oplossing. Let wel op dat het helpt te beginnen met de juiste datastructuur. Dat is al een optimalisatie op zich.

Het herschrijven van Python-algoritmes om NumPy te gebruiken neemt nogal wat werk in beslag aangezien array objecten gedeclareerd moeten worden volgens de syntaxis van NumPy. Maar NumPy gebruikt gelukkig Python's bestaande idiomen voor de werkelijke wiskundige operaties (+, -, etc,) Dus het switchen naar NumPy is niet te desoriënterend op de lange termijn.

Op de volgende pagina: Het gebruik van C

4. Gebruik een C-library

Zoals je op de vorige pagina al kon lezen is NumPy's gebruik van libraries die geschreven zijn in C een goede manier om het een en ander te emuleren. Als er een bestaande C-library is die doet wat je nodig hebt, kan je met Pythons ecosysteem de library verbinden en z'n snelheid gebruiken.

De bekendste manier om dit voor elkaar te krijgen is door gebruik de maken van Python's ctypes library. Het is de beste optie om mee te beginnen aangezien ctypes grotendeels compatible is met andere Python-applicaties (en runtimes). Maar er zijn uiteraard nog veel meer mogelijkheden. Het CFFI-project geeft je een wat elegantere interface naar C. En dan is er ook nog Cython waarmee je externe libraries kan wrappen al zal je je zelf wel moeten storten op de markup van Cython.

5. Converteren naar Cython

Als je snelheid wil, gebruik C, geen Python. Maar voor Python-gebruikers kan het schrijven van C-code voor een hoop afleidingen zorgen: Het leren van de C-syntaxis, het doorploegen van de toolchain (wat is er nu weer mis met m'n header-files?) enzovoort.

Cython geeft Python-gebruikers de mogelijkheid de snelheid van C te gebruiken. Bestaande Python code kan incrementeel worden geconverteerd naar C (Door eerst de code te converteren naar C met Cython en daarna de type-annotaties toe te voegen voor meer snelheid.)

Cython is overigens geen magische oplossing voor al je problemen. Code die geconverteerd is met Cython levert je hooguit 15 tot 50 procent snelheidswinst op omdat de meeste optimalisaties op dat niveau vooral de focus leggen op het verminderen van de overhead van de Python interpreter. De grootste snelheidswinsten komen alleen als je type-annotaties voor een Cython-module levert waardoor de code wordt geconverteerd naar Pure C.

CPU-specifieke code zal het meeste voordeel hebben van Cython. Als je hebt geprofileerd (dat heb je toch wel gedaan hè?) welke delen van je code de meeste CPU-tijd gebruiken, zal je zien dat deze onderdelen perfecte kandidaten zijn voor een Cython-conversie. Code die vooral I/O-gebonden is zoals langdurige netwerk-operaties zullen maar weinig of zelfs geen voordeel hebben van Cython.

Op de volgende pagina: Multitprocessing?

6. Ga parallel met multiprocessing

Traditionele Python-apps (die geïmplementeerd zijn in CPython) voeren maar één thread per keer uit om problemen te voorkomen. Hiervoor wordt gebruik gemaakt van de beruchte Global Interpreter Lock (GIL). Hoewel er goede redenen zijn voor het gebruik van deze lock, kan dat best wel een stapje minder.

De GIL is een stuk efficiënter geworden de laatste tijd (nog een reden om Python 3 te gebruiken in plaats van Python 2) maar het hoofdprobleem blijft bestaan. Een CPython-app kan multithreaded zijn, maar CPython staat het uitvoeren van deze threads in parallel op meerdere cores niet echt toe.

Daar kan je omheen werken door de multiprocessing module te gebruiken waarmee meerdere instanties van de Python interpreter op meerdere cores uitgevoerd kunnen worden. States kunnen worden gedeeld op de manier die overeen komt met gedeeld geheugen of serverprocessen, en data kan worden uitgewisseld via queues of pipes.

Je moet de state nog steeds handmatig beheren tussen de processen door en er is geen kleine hoeveelheid aan overhead gemoeid in het starten van meerdere instanties van Python en het uitwisselen van objecten tussen die instanties. Maar voor processen die langdurig draaien die voordeel halen uit het gebruik van het parallelliseren van meerdere cores is de multiprocessing-library erg handig.

Een handig weetje is dat Python modules en packages (als het eerder genoemde NumPy) die gebruik maken van C-libraries maken geen gebruik van GIL. Nog een reden ze te gebruiken voor een snelheidsboost.

7. Weet wat je libraties doen

Hoe handig is het om simpelweg include xyz te typen om zo gebruik te maken van het werk van talloze andere programmeurs? Je moet je er wel van bewust zijn dat 3rd-party libraries de prestaties van je applicatie flink kunnen beïnvloeden, en niet altijd op een positieve manier.

Dit uit zich soms op vanzelfsprekende manieren, wanneer een module van een ander zorgt voor een bottleneck (nogmaals, profiling helpt). En soms is het helemaal niet zo vanzelfsprekend. Pyglet is bijvoorbeeld een handige library voor het maken van grafische applicaties binnen een window. Maar deze activeert wel automatisch een debugmodus wat de prestatie van je applicatie flink beïnvloedt totdat deze expliciet wordt uitgezet. Je zou het je in eerste instantie niet eens realiseren tot je de documentatie leest. Daarom, lees en informeer jezelf.

Op de volgende pagina: Externe factoren

8. Wees bewust van het platform waar je op werkt

Python is multiplatform, maar dat betekent niet dat elk besturingssysteem z'n eigenaardigheden kent. Het gebruik van Windows, Linux, OS X zal in eerste instantie misschien niets afdoen aan je ervaringen, maar houd rekening met dat platform specifieke eisen als de naamgeving van paden. Gelukkig zijn daar handige helper-functies voor.

Het begrijpen van het platform waar je op werkt is ook belangrijk als het gaat om de prestaties. Onder Windows zal je bepaalde Windows-API-calls moeten aanroepen om hoge-resolutietimers te bereiken om de timer-resolutie te verhogen (Handig voor scripts die een timer-nauwkeurigheid fijner dan 15 millisecondes nodig hebben voor, bijvoorbeeld, multimedia).

9. Uitvoeren met PyPy

CPython is de meest gebruikte implementatie van Python. Deze implementatie richt zich voornamelijk op compatibiliteit en iets minder met ruwe snelheid. Programmeurs die snelheid op de eerste plaats willen hebben staan, kunnen gebruik maken van PyPy, een Python-implementatie die een JIT-compiler aan boord heeft om code-executie te versnellen.

Het is een van de makkelijkste manieren om een snelle prestatie-boost te krijgen aangezien PyPy als een directe vervanger van CPython. De meeste Python-applicaties zullen op PyPy exact hetzelfde werken als op CPython. Het komt er makkelijk gezegd op neer dat hoe beter een applicatie overweg kan mat "vanilla Python" hoe groter de kans is dat deze ook op PyPy kan draaien zonder modificaties.

Het is wel handig om PyPy te bestuderen en te testen voordat je ermee aan de slag gaat. Je zal er dan achter komen dat er veel apps zijn die grote prestatiewinsten behalen met deze oplossing. Dat komt omdat de compiler de executie bijtijds analyseert. Als je kleine scripts gebruikt die eenmalig draaien en vervolgens stoppen, ben je wellicht beter af met CPython aangezien de prestatiewinst onvoldoende zal zijn om de overhead van de JIT te overkomen.

Houd er rekening mee dat PyPy's ondersteuning voor Python 3 nog wat versies achter loopt. Op dit moment ondersteunt deze Python 3.2.5. Code die gebruik maakt van latere Python (3) functies als async en await-co-routines zullen niet werken. Ten slotte zullen Python-apps die ctypes gebruiken zich niet altijd gedragen zoals verwacht. Als je iets schrijft dat zowel op PyPy als CPython kan draaien, is het wellicht handig te kijken welke use-cases het beste werken voor een van de twee interpreters.

Andere experimenten voor het versnellen van Python middels het gebruik van JIT-compilatie zouden ook kunnen worden overwogen. Je zou kunnen kijken naar Pyjion, een Microsoft-project dat CPython uitrust met een interface voor JIT's. Microsoft heeft op dit moment een eigen JIT als proof of concept.

Op de volgende pagina: Last but not least...

10. Upgrade naar Python 3

Als je Python 2.x gebruikt zonder specifieke reden dat te blijven doen (zoals de incompatibiliteit van een module), switch dan naar Python 3.

Er zijn erg veel constructs en optimalisaties beschikbaar in Python 3 die niet beschikbaar zijn in Python 2.x. Python 3.5 zorgt er bijvoorbeeld voor dat asynchroon programmeren minder lastig is door async en await als standaard sleutelwoorden op te nemen in de syntaxis van de taal. Python 3.2 heeft een flinke upgrade uitgevoerd op de GIL waardoor Python nu veel beter overweg kan met meerdere threads.

Als je je zorgen maakt over de snelheidsverliezen tussen de verschillende Python-versies, kijk dan op de site van een van de core-ontwikkelaars. Daarop wordt bijgehouden welke prestatieveranderingen er plaatsvinden in de verschillende versies.

Nu Python volwassen is, kan je veel verbeteringen verwachten in compilertechnologie en interpreter-optimalisaties die Python nog sneller maken de komende jaren.

Dat gezegd hebbende, een sneller platform helpt natuurlijk maar deels. De prestatie van je applicaties is vooral afhankelijk van de persoon die deze applicatie schrijft dan het systeem die hem uitvoert. Gelukkig kunnen Python-fans genoeg opties gebruiken binnen het Python-ecosysteem om onze code sneller te laten draaien. Een Python-app hoeft uiteraard niet altijd pijlsnel te zijn, als deze maar snel genoeg is.