* Namen: Timo Migchielsen en Joris Wes
* Datum: 23-01-2023
* Omschrijving: Compile-time programming
# Compile-time programming
Wij gaan in dit bestand uitleggen hoe je met behulp van Compile-time programming het programma kunt versnellen. We gaan laten zien hoe er gebruik gemaakt kan worden van constexpr, consteval en constinit om berekeningen en bewerkingen tijdens de compilatie uit te voeren.
Dit bespaart een significant aantal processorcyclussen tijdens de runtime. Daarnaast zullen we ook laten zien hoe je gebruik kunt maken van parameter packs en folds, aangezien dit ook een deel van de opdracht was.
In het document wordt uitgelegd wat compile-time programming in houdt en waar het voor gebruikt wordt. Er zal ook een voorbeeld code worden gegeven, waarin we voor fibonacci en voor faculteit berekeningen gaan uitvoeren. Deze zullen worden vergelijken met runtime en compile time, zodat duidelijk te zien is wat de voordelen zijn van compile time programming.
Het gebruik van parameter packs helpt om efficiëntere en flexibelere code te schrijven.
We zullen ook dieper ingaan op het gebruik van parameter packs. Parameter packs zijn een manier om een variabele hoeveelheid parameters mee te geven aan een functie of een template. Hierdoor kan de code veel flexibeler en efficiënter gemaakt worden. Ook hiervan zullen we een voorbeeld geven. Hier zijn enkele voordelen van het gebruik van parameter packs:
- Geen aparte overloads of speciale template instantiaties te maken voor elke hoeveelheid parameters
- Gemakkelijk een variabele hoeveelheid argumenten verwerken
- Het maakt de code veel leesbaarder en overzichtelijker, aangezien je niet hoeft te herhalen dezelfde code voor verschillende hoeveelheden parameters.
- Code is meer schaalbaar, omdat je niet hoeft te werken met een vast aantal parameters.
## Wat is compile-time programming?
Compile-time programming is het uitvoeren van code tijdens het compileren van het programma, in plaats van tijdens het uitvoeren van het programma zelf. Het resultaat van dit stuk code wordt gebruikt om machine code te maken voor het programma.
Compile-time programming kan handig zijn als een programma geoptimaliseerd moet zijn voor prestaties, of waar sommige operaties gedaan moeten zijn voordat het programma gerund wordt, bijoorbeeld code generatie of constant folden. Runtime programming is handig als het programma moet reageren op de verandere status van het programma, bijvoorbeeld door input tijdens het runnen van het programma.
### constexpr
Constexpr is een functie uit C++11 die ervoor dat een variabele of functie als een constante waarde gedefineerd kan worden tijdjens de compilatie. Dit kan gebruikt worden voor functies waarvan het resultaat tijdens de compilatie berekend kan worden. De constexpr variabelen worden tijden de compilatie berekend en kunnen gebruikt worden in constante expressies.
### consteval
Consteval is een C++20 functie die vergelijkbaar is met constexpr, maar deze functie is specifiek voor functies, dus niet voor variabelen. Het geeft aan dat een fucntie tijjdjens de compilatie kan worden berekend. Deze resultaten kunnen net als de resultaten van constexpr functies aangeroepen worden in constante expressies, maar ze hebben wel een aantal beperkingen, zo kunnen consteval functies geen functies aanroepen die niet consteval zijn.
### constinit
Constinit is een C++20 functie die ook weer vergelijkbaar is met constexpr. Deze functie is het tegenovergesteld van consteval, want deze functie is namelijk alleen te gebruiken met vairabelen. Het geeft aan dat een variabele tijden de compilatie kan worden geïnitialiseerd. De waarde moet wel een constante expressie zijn. Constinit variabelen kunnen ook in constante expressies gebruikt worden, constinit variabelen mogen alleen niet gewijzig worden nadat ze geïnitialiseerd zijn.
### Parameter packs
Een parameter pack is een functie in C++ dat er voor zorgt dat een variabel getal of templateparameters gebruikt kan worden in een template. Het is een manier om variabelen naar een template te sturen, en het kan gebruikt worden flexible compile-time constructies te maken. Voor een parameter pack maakt het niet uit hoeveel variabelen er mee gestuurd worden.
### Folds
Een fold is een functie in C++ die er voor zorgt dat binaire operaties uitgevoerd kunnen worden op elementen van een parameter pack. De fold expressie zal ervoor zorgen dat de pack wordt verdeeld in individuele elementen en gebruikt de operator tussen die elementen. Bijvoorbeeld als je een + gebruikt, dan krijg je de som van elementen in de parameter pack.
## Waar wordt het voor gebruikt?
Compile-time programming wordt gebruikt voor veel verschillende onderdelen. Hieronder een paar voorbeelden:
* Complexe numerieke berekeningen: Als berekeningen tijdens compile-time worden uitgevoerd, kan dit er voor zorgen dat er tijdens runtime hogere prestaties worden behaald, dan als de berekeningen tijdens runtime nog uitgevoerd moeten worden.
* Code generatie: Compile-time programming kan gebruikt worden om code te genereren tijden compile-time, bijvoorbeeld om vaak repeterende code, zoals implemantaties te genereren, of code voor specifieke hardware architecturen.
* Optimalisatie: Het is mogelijk om code te optimalizeren tijdens compile-time, bijvoorbeeld voor het folden of inlinen van functions. Dit kan er voor zorgen dat de code tijdens runtime sneller en efficiënter uitgevoerd worden.
* Embedded systems: In embeddedstystemen kan compile-time programming handig zijn om resources te besparen tijdens runtime.
* Compile-time configuratie: Compile-time programming kan gebruikt worden om een programma te configureren tijdens compile-time. Bijvoorbeeld om bepaalde functies aan of uit te zetten, of voor specifieke parameters.
High-performance numerical computations: By performing calculations at compile-time, it can be possible to achieve much higher performance than would be possible with runtime calculations. For example, in scientific simulations, where performance is critical, it can be useful to perform certain calculations at compile-time in order to avoid the overhead of runtime calculations.
Code generation: Compile-time programming can be used to generate code at compile-time, for example, to generate boilerplate code, such as serialization/deserialization implementations, or to generate code for specific hardware architectures.
Optimization: Compile-time programming can be used to optimize code at compile-time, for example, by performing constant folding or inlining of functions. This can lead to faster and more efficient code execution.
Embedded systems: In embedded systems, where resources such as memory and processing power are often limited, it can be useful to perform certain operations at compile-time to save resources at runtime.
Domain-specific languages: Compile-time programming can be used to create domain-specific languages (DSLs) that are executed at compile-time. This can be useful for example in the definition of a programming language to express a problem in a natural and more efficient way, or to express a specific domain's problem more naturally.
Compile-time configuration: Compile-time programming can be used to configure the program at compile-time, for example, to enable or disable certain features, or to set specific parameters. This can be useful in situations where runtime configuration is not possible or desirable.
## Tutorial - Parameter packs met constexpr
Deze code maakt gebruik van een functie-template genaamd "add" om op te tellen van een variabele hoeveelheid van argumenten van elk type. Dit is ook te zien in de voorbeeld code. Er wordt een berekening gedaan met integers en met doubles, het maakt ook niet uit hoeveel getallen je meegeeft. De functie-template gebruikt een "parameter pack" genaamd "Ts" om een onbekend aantal argumenten van elk type op te nemen. Het eerste argument "T x" wordt vervolgens toegevoegd aan de som van de resterende argumenten in het parameter pack "xs" met behulp van een recursie. De som van de argumenten wordt berekend tijdens de compile-tijd dankzij constexpr.
Het gebruik van parameter packs in dit geval is handig omdat het de functie-template de flexibiliteit geeft om een variabele hoeveelheid argumenten te aannemen zonder dat er extra code hoeft te worden geschreven voor elke mogelijke hoeveelheid argumenten. Dit maakt de code efficiënter en gemakkelijker te onderhouden.
**Stap 1:** Open je code editor en maak een nieuw C++ executable project
**Stap 2:** Kopier de code hieronder in de main.cpp
```cpp=
#include <iostream>
template <typename T>
constexpr T add(T x)
{
return x;
}
/*
* Deze functietemplate definieert een parameterpack T dat 0 of meer argumenten van elk type kan aannemen.
* Het parameterpack wordt vervolgens uitgebreid met een left fold-expressie (aangeduid met drie puntjes gevolgd door de + operator)
* om de som van de elementen in het parameterpack te berekenen.
*/
template <typename T, typename... Ts>
constexpr T add(T x, Ts... xs)
{
return x + add(xs...);
}
int main()
{
// Berekend de som van 1, 2, 3 en 4 tijdens compile_time
constexpr int result = add(1, 2, 3, 4);
std::cout << "The sum of 1, 2, 3, and 4 is: " << result << std::endl;
// Berekend de som van 1.3, 2.6, 3.6 en 4.2 tijdens compile_time
constexpr double doubleResult = add(1.3, 2.6, 3.6, 4.2);
std::cout << "The sum of 1.3, 2.6, 3.6 and 4.2 is: " << doubleResult << std::endl;
return 0;
}
```
**Stap 3:** Build het project
**Stap 4:** Run nu het project
## Resultaat en conclusie
Hieronder staat wat er in de console komt te staan:
```c
The sum of 1, 2, 3, and 4 is: 10
The sum of 1.3, 2.6, 3.6 and 4.2 is: 11.7
Process finished with exit code 0
```
## Tutorial - Factiorial & Fibonacci code testen
De factorial functie is een wiskundige bewerking die het product berekent van alle positieve gehele getallen tot een bepaald getal, bijvoorbeeld 5! = 5 x 4 x 3 x 2 x 1 = 120.
De Fibonacci functie doet een berekening over een reeks getallen waarin elke term de som is van de vorige twee, meestal beginnend met 0 en 1, zoals 0,1,2,3,5,8,13.
**Stap 1:** Open je code editor en maak een nieuw C++ executable project
**Stap 2:** Kopier de code hieronder in de main.cpp
```cpp=
#include <iostream>
#include <chrono>
// Functie fibonacci_run berekend de fibonacci van getal n tijdens run-time
int fibonacci_run(int n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fibonacci_run(n - 1) + fibonacci_run(n - 2);
}
}
// Functie fibonacci_run berekend de fibonacci van getal n tijdens compile-time
constexpr int fibonacci_comp(int n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fibonacci_comp(n - 1) + fibonacci_comp(n - 2);
}
}
// Functie factorial_runtime berekend de factorial van getal n tijdens run-time
int factorial_runtime(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
// Functie fibonacci_compiletime berekend de factorial van getal n tijdens compile-time
constexpr int factorial_compiletime(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
int main() {
// Variabele n, deze wordt geïnitialiseerd tijdens compile time,
// zodat deze te gebruiken is in de functie factorial_compiletime
// Deze waarde kon niet groter dan 12, omdat dan de uitkomt van de
// factorial functie te groot zou zijn voor een integer.
constexpr int n = 12;
// Variabele m, deze wrodt geïnitialiseerd tijdens compile time,
// zodat deze te gebruiken is in de functie fibonacci_comp.
constexpr int m = 24;
// Berekenen van de tijd voor het runnen van de functie factorial_runtime
auto start = std::chrono::high_resolution_clock::now();
int fact_rt = factorial_runtime(n);
auto end = std::chrono::high_resolution_clock::now();
auto runtime_time_fact = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);
// Berekenen van de tijd voor het runnen van de functie factorial_compiletime
start = std::chrono::high_resolution_clock::now();
constexpr int fact_ct = factorial_compiletime(n);
end = std::chrono::high_resolution_clock::now();
auto compiletime_time_fact = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);
// Berekenen van de tijd voor het runnen van de functie fibonacci_run
start = std::chrono::high_resolution_clock::now();
int fibb_rt = fibonacci_run(m);
end = std::chrono::high_resolution_clock::now();
auto runtime_time_fibb = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);
// Berekenen van de tijd voor het runnen van de functie fibonacci_comp
start = std::chrono::high_resolution_clock::now();
constexpr int fibb_ct = fibonacci_comp(m);
end = std::chrono::high_resolution_clock::now();
auto compiletime_time_fibb = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);
// Printen van de factorial resultaten in de console
std::cout << "Faculteit van " << n << ": " << fact_rt << std::endl;
std::cout << "Runtime tijd: " << runtime_time_fact.count() << " nanoseconds" << std::endl;
std::cout << "Compile tijd Faculteit van " << n << ": " << fact_ct << std::endl;
std::cout << "Gebruik van compile tijd berekende waarde: " << compiletime_time_fact.count() << " nanosecondes" << std::endl;
// Printen van de Fibonacci resultaten in de console
std::cout << "Fibonacci van " << m << ": " << fibb_rt << std::endl;
std::cout << "Runtime tijd: " << runtime_time_fibb.count() << " nanoseconds" << std::endl;
std::cout << "Compile tijd Fibonacci van " << m << ": " << fibb_ct << std::endl;
std::cout << "Gebruik van compile tijd berekende waarde: " << compiletime_time_fibb.count() << " nanosecondes" << std::endl;
return 0;
}
```
**Stap 3:** Build het project
**Stap 4:** Run nu het project
## Resultaat en conclusie
Hieronder staat een voorbeeld van wat er in de console komt te staan:
```c
Faculteit van 12: 479001600
Runtime tijd: 500 nanoseconds
Compile tijd Faculteit van 12: 479001600
Gebruik van compile tijd berekende waarde: 100 nanosecondes
Fibonacci van 24: 46368
Runtime tijd: 352500 nanoseconds
Compile tijd Fibonacci van 24: 46368
Gebruik van compile tijd berekende waarde: 100 nanosecondes
```
De uitslag toont de berekeningsresultaten van de factorial en Fibonacci functies. Het toont ook de tijd die nodig was om de berekeningen uit te voeren tijdens runtime en compile tijd.
De factorial van 12 is 479001600 en de Fibonacci van 24 is 46368. De runtime-tijd voor de factorial van 12 was 500 nanoseconden en de runtime-tijd voor de Fibonacci van 24 was 376500 nanoseconden. De compile-tijd voor beide berekeningen was 100 nanoseconden.
Er is een groot verschil tussen de berekeningen van de run-tijd en compile-tijd omdat de code tijdens de compile-tijd geoptimaliseerd wordt.
Tijdens runtime wordt de code uitgevoerd zoals het is geschreven zonder enige voorafgaande optimalisatie. Dit kan leiden tot presentaties die trager zijn. De reden hiervoor is, is dat de berekeningen van factorial en Fibonacci in de compile-tijd worden uitgevoerd. Deze berekeningen worden eerst geoptimaliseerd, wat leidt tot snellere prestaties tijdens runtime.
# Referenties
C++ reference (constexpr):
https://en.cppreference.com/w/cpp/language/constexpr
C++ reference (consteval):
https://en.cppreference.com/w/cpp/language/consteval
C++ reference (constinit):
https://en.cppreference.com/w/cpp/language/constinit
C++ reference (parameter packs): https://en.cppreference.com/w/cpp/language/parameter_pack
C++ reference (voordelen compile programming):
https://www.fluentcpp.com/2019/03/01/compile-time-computation-in-c/