Pieter Edelman
12 February

Het schrijven van parallelle software is in de praktijk nog altijd lastig. Je blijft nu eenmaal tegen onvoorziene zaken aanlopen als je het probleem niet op alle niveaus begrijpt, meent Klaas van Gend.

Multicoresoftware zou anno 2019 makkelijker te schrijven moeten zijn dan ooit. Moderne programmeertalen zoals Scala en Rust worden volwassen, programmeerraamwerken worden steeds makkelijker te gebruiken en ook C# en good old C++ omarmen parallellisme in hun standaardbibliotheken.

De praktijk valt echter nog altijd vies tegen. De boel blijkt toch lastig te synchroniseren en wanneer de software eenmaal werkt, draait die vaak nauwelijks of helemaal niet sneller op een multicore processor. En tot overmaat van ramp heeft die de neiging allerlei ongrijpbare fouten te vertonen.

Parallel programmeren is gewoon een erg taai onderwerp waarbij je tegen allerlei subtiele, onverwachte effecten aanloopt als je niet op alle niveaus begrijpt wat er gebeurt, denkt Klaas van Gend, softwarearchitect bij Sioux. ‘Ik heb weleens mensen horen praten over het delen van nodes op een supercomputer door middel van virtuele machines. Maar die verprutsen elkaars processorcache; ze zitten elkaar gewoon in de weg.’

Het probleem is volgens Van Gend ook dat veel ontwikkelaars geen gedegen basis hebben meegekregen tijdens hun informaticaopleiding. ‘Op de universiteit ging het over Dijkstra, dus mutexes, locks en condition variables. Maar op het moment dat je een lock aanzet, dan zorg je er alleen voor dat de code op één core wordt uitgevoerd terwijl de andere een tijdje niets doen. Dus je leert eigenlijk alleen maar hoe je niet voor multicore programmeert’, stelt hij.

Vandaar dat Van Gend de multicoretraining van zijn oude werkgever Vector Fabrics weer uit de mottenballen heeft gehaald. Dit bedrijf richtte zich tot een paar jaar terug op tooling om inzicht te geven in de perikelen rond parallelle software. Samen met cto Jos van Eijndhoven en andere werknemers verzorgde Van Gend een training rond het onderwerp. Vector Fabrics ging in 2016 failliet, maar bij zijn huidige werkgever merkte Van Gend dat de problematiek nog steeds relevant is. Na de training daar een keer opnieuw te hebben gegeven, biedt hij die nu onder de vlag van High Tech Institute ook voor derden aan.

Een probleem over alle niveaus

Een van de belangrijke thema’s bij het schrijven van parallelle software is dat het een probleem is dat uitgesproken over meerdere niveaus heen werkt, vertelt Van Gend. Hij maakt dit punt altijd met een simpel voorbeeldje: Conways Game of Life, de cellulaire automaat waarbij vakjes in een raster bij elke nieuwe ronde zwart of wit worden afhankelijk van de status van hun directe buren. ‘Op het onderste niveau van je programma moet je daarvoor controleren wat je buurcellen zijn. Dat kan met twee for-loopjes. En vervolgens heb je een loop voor een complete rij, en daarboven voor de complete verzameling van rijen.’

‘De meeste programmeurs zullen beginnen te parallelliseren bij die onderste loops. Dat is heel natuurlijk, want dat is een stukje code dat je nog kunt snappen, dat past nog in je hoofd. Maar het heeft veel meer zin om op een hoger niveau te gaan zitten en die buitenste lus te pakken. Dan deel je dus het veld op in meerdere blokken van rijen en is je workload per core veel groter.’

Als je op die manier naar de materie gaat kijken, blijkt al snel dat er heel veel dingen zijn om op te letten. Zo zijn er ook programma’s waarbij de belasting variabel is. ‘We hebben bijvoorbeeld een oefening om de eerste honderd priemgetallen uit te rekenen. Daar zit al meer dan een factor honderd tussen priemtgetal tien en priemgetal negenennegentig. Dan moet je aan load balancing gaan doen.’

Ook zijn er verschillen in wát je kunt parallelliseren: de data of de taak. ‘Dataparallellisme is over het algemeen voor hele specifieke toepassingen geschikt, maar verder kom je al snel met een soort decompositie van je taak. Dat kan bijvoorbeeld met een actor-model of met een Kahn-procesnetwerk, maar daar kan dataparallellisme wel weer een onderdeel van zijn. In de praktijk zie je dat je altijd uitkomt op mengvormen.’

Het gaat ook lang niet alleen om algoritmes; juist de onderliggende hardware speelt een sleutelrol. Als de programmeur bijvoorbeeld geen rekening houdt met de cachemechanismes van de processor, kan het probleem van false sharing ontstaan. ‘Daar heb ik hele grote applicaties mee op hun knieën zien gaan’, vertelt Van Gend. ‘Stel dat je twee threads hebt die allebei metrieken aan het verzamelen zijn. Als je die onhandig opdeelt, kunnen tellers van verschillende threads in dezelfde cachelijn terechtkomen. De twee processoren moeten dan tegelijk met diezelfde cachelijn bezig zijn en je cachemechanisme is voortdurend bezig de lijnen heen en weer te slepen. Dat kost echt veel performance.’

Om die reden is Van Gend dan ook sceptisch over de inzet van hogereniveautalen in multicoreontwerpen; die hebben de neiging de details over de geheugenlay-out te abstraheren. ‘Met een taal als C++ is het nog heel helder dat je met basisprimitieven bezig bent en kun je dat netjes zien. Maar hogereniveautalen walsen vaak over de details van de datatypes heen, met als gevolg dat het nooit echt lekker kan lopen.’

Sowieso denkt Van Gend dat nieuwe talen geen panacee zijn voor de multicoreproblematiek. Ze gaan in de regel namelijk uit van een specifieke aanpak, die helemaal niet goed hoeft te passen bij de applicatie. ‘Talen als Scala of Rust zetten zwaar in op het actor-model om threading makkelijker te maken. Als je dat model maar deels snapt, dan kom je toch in de problemen. Het werkt goed voor sommige specifieke situaties, maar je kunt het niet overal voor gebruiken.’

Verkeerde aanname

De moderne versies van C++ bieden bovendien ook toevoegingen om parallel programmeren mogelijk te maken. ‘Atomics zitten er nu bijvoorbeeld helemaal in. Daarmee kun je vaak data uitwisselen zonder dat er iets wordt stilgezet. Er wordt ook gewerkt aan een library waarmee de locking helemaal niet meer zichtbaar is voor gebruikers. Als het al nodig is, gebeurt het zonder dat de gebruiker het ziet en ook met zo kort mogelijke scope, dus de lock wordt zo snel mogelijk weer vrijgegeven’, weet Van Gend.

Hier is het eveneens belangrijk om te snappen waar je mee bezig bent. Van Gend is bijvoorbeeld een stuk minder enthousiast over de execution policies-toevoeging aan de standaardbibliotheek in C++17. Hiermee is een reeks basisalgoritmes zoals find, count, sort en transform parallel te draaien door simpelweg een extra parameter mee te geven in de functieaanroep. ‘Maar dat werkt alleen voor wat academische voorbeelden; in de praktijk zal het niet voldoen’, denkt Van Gend. ‘Die api’s zijn gebaseerd op een foute basisaanname. En in de C#-api’s hebben ze dezelfde fout gemaakt.’

Het probleem is dat je met de aanpak alleen losse stapjes kunt maken. ‘Die stimuleert het individueel parallelliseren van elke operatie. Je deelt je dataset bij elke bewerking opnieuw op, doet wat, dan maak je er weer een geheel van en ga je naar de volgende operatie. Het is telkens parallel, sequentieel, parallel, sequentieel, enzovoorts. Dat is conceptueel wel heel helder, maar je moet de hele tijd wachten tot alle threads klaar zijn en weer doorgaan. Zonde van de tijd. Bij een bibliotheek als Openmp daarentegen wordt de hele set operaties gewoon over de threads verdeeld. Daarmee wordt dus niet nodeloos gewacht.’

De gcc-compiler heeft ondersteuning voor deze parallelle functies er ook nog niet in zitten. Visual Studio wel, want de toevoegingen komen uiteindelijk van Microsoft af. ‘Het grappige is dat Microsoft ook een flink deel heeft betaald van het Par Lab aan de universiteit van Berkeley. Dat heeft geresulteerd in een behoorlijk grote verzameling design patterns voor parallel programmeren, die ik uitvoerig behandel in de training. Daar heeft Microsoft laten zien dat ze precies begrijpen hoe het wel moet.’