Skip to content

Commit

Permalink
Work on data-structures
Browse files Browse the repository at this point in the history
  • Loading branch information
nowox committed May 2, 2020
1 parent 759372c commit 7985d99
Show file tree
Hide file tree
Showing 12 changed files with 1,155 additions and 122 deletions.
4 changes: 4 additions & 0 deletions _templates/layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% extends "!layout.html" %}
{% block extrahead %}
<link href="{{ pathto("_static/custom.css", True) }}" rel="stylesheet" type="text/css">
{% endblock %}
2 changes: 1 addition & 1 deletion assets/figures
Submodule figures updated 41 files
+0 −0 dist/data-structure/binary-tree.svg
+0 −0 dist/data-structure/delete.svg
+0 −0 dist/data-structure/double.svg
+0 −0 dist/data-structure/floyd.svg
+252 −0 dist/data-structure/hash-linear.svg
+226 −161 dist/data-structure/hash-table.svg
+0 −0 dist/data-structure/heap.svg
+466 −0 dist/data-structure/list-in-memory.svg
+0 −0 dist/data-structure/list.svg
+0 −0 dist/data-structure/loop.svg
+0 −0 dist/data-structure/overview.svg
+0 −0 dist/data-structure/pop-push-shift-unshift.svg
+0 −0 dist/data-structure/queue.svg
+223 −0 dist/data-structure/ring.svg
+425 −0 dist/data-structure/simple-insert.svg
+388 −0 dist/data-structure/simple.svg
+631 −0 dist/data-structure/static-linked-list.svg
+710 −0 dist/data-structure/tree.svg
+1,405 −0 dist/memory/fragmentation.svg
+697 −0 dist/memory/mmu.svg
+0 −297 dist/recursive-data-structure/simple.svg
+ src/data-structure/binary-tree.vsdx
+ src/data-structure/delete.vsdx
+ src/data-structure/double.vsdx
+ src/data-structure/floyd.vsdx
+ src/data-structure/hash-linear.vsdx
+ src/data-structure/hash-table.vsdx
+ src/data-structure/heap.vsdx
+ src/data-structure/list-in-memory.vsdx
+ src/data-structure/list.vsdx
+ src/data-structure/loop.vsdx
+ src/data-structure/overview.vsdx
+ src/data-structure/pop-push-shift-unshift.vsdx
+ src/data-structure/queue.vsdx
+ src/data-structure/ring.vsdx
+ src/data-structure/simple-insert.vsdx
+ src/data-structure/simple.vsdx
+ src/data-structure/static-linked-list.vsdx
+ src/data-structure/tree.vsdx
+ src/memory/fragmentation.vsdx
+ src/memory/mmu.vsdx
34 changes: 34 additions & 0 deletions assets/src/memoize.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include <stdio.h>
#include <stdbool.h>

#define SIZE 1000

bool cache_input[SIZE] = { false };
int cache_output[SIZE];

int memoize(int input, int output) {
cache_input[input % SIZE] = true;
cache_output[input % SIZE] = output;
return output;
}

bool memoize_has(int input) {
return cache_input[input % SIZE];
}

int memoize_get(int input) {
return cache_output[input % SIZE];
}

int fib(int n)
{
if (memoize_has(n)) return memoize_get(n);
if (n < 2) return 1;
return memoize(n, fib(n - 1) + fib(n - 2));
}

int main() {
for (int i = 0; i < 40; i++) {
printf("%d\n", fib(i));
}
}
7 changes: 6 additions & 1 deletion content/20-datatype.rst
Original file line number Diff line number Diff line change
Expand Up @@ -733,10 +733,15 @@ L'utilisation d'un type énuméré peut être la suivante :
}
}
Type incomplet
==============

Un type incomplet est un qualificatif de type de donnée décrivant un objet dont sa taille en mémoire n'est pas connue.

Type vide (*void*)
==================

Le type ``void`` est particulier car c'est un type qui ne vaut rien. Il est utilisé comme type de retour pour les fonctions qui ne retournent rien :
Le type ``void`` est particulier. Il s'agit d'un type dit **incomplet** car la taille de l'objet qu'il représente en mémoire n'est pas connue. Il est utilisé comme type de retour pour les fonctions qui ne retournent rien :

.. code-block:: c
Expand Down
140 changes: 140 additions & 0 deletions content/40-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,146 @@ En des termes plus corrects, mais nous verrons cela au chapitre sur les pointeur

Retenez simplement que lors d'un passage par référence, on cherche à rendre la valeur passée en paramètre modifiable par le *caller*.

Récursion
=========

La récursion, caractère d'un processus, d'un mécanisme récursif, c'est à dire qui peut être répété un nombre indéfini de fois par l'application de la même règle, est une méthode d'écriture dans laquelle une fonction s'appelle elle même.

Au chapitre sur les fonctions, nous avions donné l'exemple du calcul de la somme de la suite de fibonacci jusqu'à ``n`` :

.. code-block:: c
int fib(int n)
{
int sum = 0;
int t1 = 0, t2 = 1;
int next_term;
for (int i = 1; i <= n; i++)
{
sum += t1;
next_term = t1 + t2;
t1 = t2;
t2 = next_term;
}
return sum;
}
Il peut sembler plus logique de raisonner de façon récursive. Quelque soit l'itération à laquelle l'on soit, l'assertion suivante est valable :

fib(n) == fib(n - 1) + fib(n - 2)

Donc pourquoi ne pas réécrire cette fonction en employant ce caractère récursif ?

.. code-block:: c
int fib(int n)
{
if (n < 2) return 1;
return fib(n - 1) + fib(n - 2);
}
Le code est beaucoup plus simple a écrire, et même à lire. Néanmoins cet algorithme est notoirement connu pour être mauvais en terme de performance. Calculer ``fib(5)`` revient à la chaîne d'appel suivant.

Cette chaîne d'appel représente le nombre de fois que ``fib`` est appelé et à quel niveau elle est appelée. Par exemple ``fib(4)`` est appelé dans ``fib(5)`` :

.. code-block:: text
fib(5)
fib(4)
fib(3)
fib(2)
fib(1)
fib(2)
fib(1)
fib(0)
fib(3)
fib(2)
fib(1)
fib(0)
fib(1)
Si l'on somme le nombre de fois que chacune de ces fonctions est appelée :

.. code-block:: text
fib(5) 1x
fib(4) 1x
fib(3) 2x
fib(2) 3x
fib(1) 4x
fib(0) 2x
-----------
fib(x) 13x
Pour calculer la somme de fibonacci, il faut appeler 13 fois la fonction. On le verra plus tard mais la complexité algoritmique de cette fonction est dite :math:`O(2^n)`. C'est à dire que le nombre d'appels suit une relation exponentielle. La réelle complexité est donnée par la relation :

.. math::
T(n) = O\left(\frac{1+\sqrt(5)}{2}^n\right) = O\left(1.6180^n\right)
Ce terme 1.6180 est appelé `le nombre d'or <https://fr.wikipedia.org/wiki/Nombre_d%27or>`__.

Ainsi pour calculer fib(100) il faudra sept cent quatre-vingt-douze trillions soixante-dix mille huit cent trente-neuf billions huit cent quarante-huit milliards trois cent soixante-quatorze millions neuf cent douze mille deux cent quatre-vingt-douze appels à la fonction `fib` (792'070'839'848'374'912'292). Pour un processeur Core i7 (2020) capable de calculer environ 100 GFLOPS (milliards d'opérations par seconde), il lui faudra tout de même 251 ans.

En revanche, dans l'approche itérative, on constate qu'une seule boucle ``for``. C'est à dire qu'il faudra seulement 100 itérations pour calculer la somme.

Généralement les algorithmes récursifs (s'appelant eux même) sont moins performants que les algorithmes itératifs (utilisant des boucles). Néanmoins il est parfois plus facile d'écrire un algorithme récursif.

Notons que tout algorithme récursif peut être écrit en un algorithme itératif, mais ce n'est pas toujours facile.

Mémoïsation
===========

En informatique la `mémoïsation <https://fr.wikipedia.org/wiki/M%C3%A9mo%C3%AFsation>`__ est une technique d'optimisation du code souvent utilisée conjointement avec des algorithmes récursifs. Cette technique est largement utilisée en `programmation dynamique <https://fr.wikipedia.org/wiki/Programmation_dynamique>`__.

Nous l'avons vu précédemment, l'algorithme récursif du calcul de la somme de la suite de Fibonacci n'est pas efficace du fait que les mêmes appels sont répétés un nombre inutiles de fois. La parade est de mémoriser pour chaque appel de ``fib``, la sortie correspondante à l'entrée.

Dans cet exemple nous utiliserons un mécanisme composé de trois fonctions :

- ``int memoize(Cache *cache, int input, int output)``
- ``bool memoize_has(Cache *cache, int input)``
- ``int memoize_get(Cache *cache, int input)``

La première fonction mémorise la valeur de sortie ``output`` liée à la valeur d'entrée ``input``. Pour des raisons de simplicité d'utilsation, la fonction retourne la valeur de sortie ``output``.

La seconde fonction ``memoize_has`` vérifie si une valeur de correspondance existe pour l'entrée ``input``. Elle retourne ``true`` en cas de correspondance et ``false`` sinon.

La troisième fonction ``memoize_get`` retourne la valeur de sortie correspondante à la valeur d'entrée ``input``.

Notre fonction récursive sera ainsi modifiée comme suit :

.. code-block:: c
int fib(int n)
{
if (memoize_has(n)) return memoize_get(n);
if (n < 2) return 1;
return memoize(n, fib(n - 1) + fib(n - 2));
}
Quant aux trois fonctions utilitaires, voici une proposition d'implémentation. Notons que cette implémentation est très élémentaire et n'est valable que pour des entrées inférieures à 1000. Il sera possible ultérieurement de perfectionner ces fonctions, mais nous aurons pour cela besoin de concepts qui n'ont pas encore été abordés, tels que les structures de données complexes.

.. code-block:: c
#define SIZE 1000
bool cache_input[SIZE] = { false };
int cache_output[SIZE];
int memoize(int input, int output) {
cache_input[input % SIZE] = true;
cache_output[input % SIZE] = output;
return output;
}
bool memoize_has(int input) {
return cache_input[input % SIZE];
}
int memoize_get(int input) {
return cache_output[input % SIZE];
}
------

.. exercise:: Dans la moyenne
Expand Down
51 changes: 51 additions & 0 deletions content/55-memory-management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Lorsqu'un programme à besoin de mémoire, il peut générer un appel système p

L'allocation se fait sur le `tas` (*heap*) qui est de taille variable. À chaque fois qu'un espace mémoire est demandé, ``malloc`` recherche dans le segment un espace vide de taille suffisante, s'il ne parvient pas, il exécute l'appel système `sbrk <https://en.wikipedia.org/wiki/Sbrk>`__ qui permet de déplacer la frontière du segment mémoire et donc d'agrandir le segment.

.. _fig_allocation:
.. figure:: ../assets/figures/dist/memory/malloc.*

Mémoire de programme
Expand Down Expand Up @@ -265,3 +266,53 @@ allez piétiner les zones mémoires voisines sans en avoir la permission.
Le compilateur (en réalité l'éditeur de liens - le *linker*) vous permet
de spécifier la taille de la pile ; c'est une de ses nombreuses options.

Variables automatiques
======================

Une variable est dite *automatique* lorsque sa déclaration est faite au sein d'une fonction. La variable d'itération ``int i`` dans une boucle ``for`` est dite automatique. C'est à dire que le compilateur à le choix de placer cette variable :

- sur la pile ;
- dans un registre mémoire processeur.

Jadis, le mot clé ``register`` était utiliser pour forcer le compilateur à placer une variable locale dans un registre processeur pour obtenir de meilleures performances. Aujourd'hui, les compilateurs sont assez malins pour déterminer automatiquement les variables souvent utilisées.

Fragmentation mémoire
=====================

On peut observer à la figure :numref:`fig_allocation` qu'après un appel successif de ``malloc`` et de ``free`` des espaces mémoires non utilisés peuvent apparaître entre des régions utilisées. Ces *trous* sont appelés fragmentation mémoire.

Dans la figure suivante, on suit l'évolution de l'utilisation du *heap* au cours de la vie d'un programme. Au début ➀, la mémoire est libre. Tant que de la mémoire est allouée sans libération (``free``), aucun problème de fragmentation ➁. Néanmoins, après un certain temps la mémoire devient fragmentée ➂ ; il reste dans cet exemple 2 emplacements de taille 2, un emplacement de taille 5 et un emplacement de taille 8. Il est donc impossible de réserver un espace de taille 9 malgré que l'espace cumulé libre est suffisant.

.. figure:: ../assets/figures/dist/memory/fragmentation.*

Dans une petite architecture, l'allocation et la libération fréquente d'espaces mémoire de taille arbitraire est malvenue. Une fois que la fragmentation mémoire est installée, il n'existe aucun moyen de soigner le mal si ce n'est au travers de l'ultime solution de l'informatique : `éteindre puis redémarrer <https://www.youtube.com/watch?v=nn2FB1P_Mn8>`__.

MMU
---

Les systèmes d'exploitations modernes (Windows, Linux, macOS...) utilisent tous un dispositif matériel nommé `MMU <https://en.wikipedia.org/wiki/Memory_management_unit>`__ pour *Memory Management Unit*. La MMU est en charge de créer un espace mémoire **virtuel** entre l'espace physique. Cela crée une indirection supplémentaire mais permet de réorganiser la mémoire physique sans compromettre le système.

En pratique l'espace de mémoire virtuelle est toujours beaucoup plus grand que l'espace physique. Cela permet de s'affranchir dans une large mesure de problèmes de fragmentation car si l'espace virtuel est suffisament grand, il y aura statistiquement plus de chance d'y trouver un emplacement non utilisé.

La programmation sur de petites architectures matérielles (microcontrôleurs, DSP) ne possèdent pas de MMU et dès lors l'allocation dynamique est généralement à proscrire à moins qu'elle soit faite en connaissance de cause et en utilisant des mécanisme comme les *memory pool*.

Dans la figure ci-dessous. La mémoire physique est représentée à droite en termes de pages mémoires physiques (*Physical Pages* ou **PP**). Il s'agit de blocs mémoires contigus d'une taille fixe, par exemple 64 kB. Chaque page physique est mappée dans une table propre à chaque processus (programme exécutable). On y retrouve quelques proriétés utiles à savoir est-ce que la page mémoire est accessible en écriture, est-ce qu'elle peut contenir du code exécutable ? Une propriété peut indiquer par exemple si la page mémoire est valide. Chacune de ces entrées est considérée comme une page mémoire virtuelle (*virtual page* **VP**).

.. figure:: ../assets/figures/dist/memory/mmu.*

Erreurs de segmentation (*segmentation fault*)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Lorsqu'un programme tente d'accéder à un espace mémoire qui n'est pas mappé dans la MMU, ou que cet espace mémoire ne permet pas le type d'accès souhaité : par exemple une écriture dans une page en lecture seule. Le système d'exploitation tue le processus avec une erreur *Segmentation Fault*. C'est la raison pour laquelle, il n'est pas systématique d'avoir une erreur de segmentation en cas de jarinage mémoire. Tant que les valeurs modifées sont localisées au sein d'un bloc mémoire autorisé, il n'y aura pas d'erreur.

L'erreur de segmentation est donc générée par le système d'exploitation en lèvant le signal **SIGSEGV** (Violation d'accès à un segment mémoire, où erreur de segmentation).

Memory Pool
-----------

Un *memory pool* est une méthode faisant appel à de l'allocation dynamique de blocs de taille fixe. Lorsqu'un programme doit très régulièrement allouer et désalouer de la mémoire, il est préférable que les blocs mémoire ait une taille fixe. De cette façon, après un ``free``, la mémoire libérée est assez grande pour une allocation ultérieure.

Lorsqu'un programme est exécuté sous Windows, macOS ou Linux, l'allocation dynamique standard ``malloc``, ``calloc``, ``realloc`` et ``free`` sont performants et le risque de crash dû à une fragmentation mémoire est rare.

En revanche lors de l'utilisation sur de petites architectures (microcontrôleurs) qui n'ont pas de système sophistiqués pour gérer la mémoire, il est parfois nécessaire d'écrire son propre système de gestion de mémoire.

21 changes: 21 additions & 0 deletions content/77-algorithms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,27 @@ Programmation dynamique

.. todo:: Compléter

Algorithmes de tris
===================

Heap Sort
---------



.. code-block:: text
8
|
----+----
/ \
4 12
-- --
/ \ / \
20 6 42 14
/ \ / \ / \ / \
11 3 35 7 9 11 50 16
-----

.. exercise:: Intégrateur de Kahan
Expand Down
Loading

0 comments on commit 7985d99

Please sign in to comment.