Pipeline -- Comment rendre les CPU plus rapides

Sans augmenter la fréquence d'horloge, les CPU peuvent-ils encore devenir plus rapides ? La réponse est oui. La technologie du pipeline est l'une des méthodes les plus importantes. Ce cours utilise du code Python et une sortie visuelle de type diagramme de Gantt pour montrer comment le pipeline améliore grandement l'efficacité d'exécution des instructions.


Analogie du quotidien : chaîne de lavage de voitures

Imaginez que vous exploitez une station de lavage de voitures. Il existe deux façons de procéder :

Méthode 1 : une seule personne tout-en-un (sans pipeline)

Un employé gère toutes les étapes de lavage d'une voiture, du début à la fin — rinçage, mousse, brossage, séchage. Ce n'est qu'après avoir terminé une voiture qu'il peut commencer la suivante. Chaque voiture prend 40 minutes, donc seulement 1.5 voitures peuvent être lavées par heure.

Méthode 2 : division du travail en pipeline (avec pipeline)

Quatre employés s'occupent chacun d'une étape — rinçage, mousse, brossage, séchage. Une fois la première voiture entrée dans l'étape de rinçage, celui qui s'occupe du rinçage peut commencer à rincer la deuxième voiture, tandis que celui qui s'occupe de la mousse fait mousser la première voiture. Les quatre personnes sont occupées en même temps, et une voiture est terminée toutes les 10 minutes.

La deuxième méthode ne rend aucune étape individuelle plus rapide (le rinçage prend toujours 10 minutes), mais en faisant chevaucher les étapes de différentes voitures, le débit global est grandement amélioré.

C'est l'idée centrale du pipeline du CPU : chevaucher l'exécution des différentes étapes de différentes instructions.


Exécution série vs. exécution en pipeline

Le problème de l'exécution série

En reprenant le cours 12, chaque instruction passe par quatre étapes : Récupération (IF), Décodage (ID), Exécution (EX), Écriture en retour (WB).

Dansl'exécution série (sans pipeline)Dans ce mode, le CPU doit exécuter entièrement les quatre étapes de l'instruction courante avant de pouvoir commencer à récupérer l'instruction suivante.

周期:  1    2    3    4    5    6    7    8    9    10   11   12
I1:   [IF] [ID] [EX] [WB]
I2:                      [IF] [ID] [EX] [WB]
I3:                                          [IF] [ID] [EX] [WB]

3 条指令 = 12 个周期

Avantages de l'exécution en pipeline

Dansl'exécution en pipelineDans ce mode, une fois que l'instruction I1 termine l'étape de récupération (IF), l'unité de récupération du CPU devient inactive. Au lieu de la laisser inactive, vous pouvez immédiatement commencer à récupérer l'instruction suivante I2.

De même, lorsque I1 termine le décodage (ID) et entre dans l'étape d'exécution, l'unité de décodage commence à traiter I2, tandis que l'unité de récupération commence à traiter I3.

周期:  1    2    3    4    5    6    7
I1:   [IF] [ID] [EX] [WB]
I2:        [IF] [ID] [EX] [WB]
I3:             [IF] [ID] [EX] [WB]

3 条指令 = 6 个周期(比串行快了一倍!)

Notez les nombres clés :

  • Sans pipeline : n instructions = 4n cycles
  • Avec pipeline : n instructions = 4 + (n-1) = n + 3 cycles (c'est-à-dire qu'une fois le pipeline rempli, une instruction se termine à chaque cycle)

Lorsque n est grand, l'accélération due au pipeline approche 4 (le nombre d'étages du pipeline).

Le pipeline ne permet pas d'exécuter une seule instruction plus rapidement (chaque instruction a toujours besoin de 4 cycles), mais il améliore grandement le « débit » — le nombre d'instructions terminées par unité de temps.


Problème d'« aléa » de pipeline

Le pipeline est une bonne chose, mais il n'est pas sans coût. Il existe trois « aléas » typiques qui peuvent bloquer le pipeline :

Types d'aléasAnglaisDescription du problèmeanalogie avec la vie
Aléa structurelAléa structurelDeux étages se disputent la même ressource matérielle (par exemple, les deux doivent accéder à la mémoire)Au lavage de voiture, il n'y a qu'un seul pistolet à eau haute pression, et le rinçage initial comme le rinçage final doivent l'utiliser, ils doivent donc faire la queue.
Aléa de donnéesAléa de donnéesL'instruction suivante dépend du résultat de l'instruction précédente, mais le résultat n'a pas encore été réécritPour préparer du porc braisé, vous devez d'abord couper la viande avant de la mettre dans la marmite. Si la personne qui coupe la viande n'a pas terminé, le cuisinier ne peut qu'attendre.
Aléa de contrôleAléa de contrôleLorsqu'on rencontre une instruction de branchement, on ne sait pas quelle instruction charger ensuite.Le menu indique : « Si le client commande un plat épicé, passez à une autre recette. » Avant que le client ne décide, la cuisine ne sait pas quoi préparer.

Les processeurs modernes utilisent diverses techniques pour atténuer ces aléas :

  • Contournement de données (Data Forwarding): Sans attendre que le résultat soit réécrit dans le registre, on « vole » directement les données de la sortie de l'ALU pour que l'instruction suivante les utilise
  • Prédiction de branchementPrédiction de branchement : on devine la direction du branchement et on précharge. Si l'on devine correctement, on gagne ; sinon, on vide le pipeline et on recommence.
  • Exécution dans le désordreExécution dans le désordre : ne pas suivre l'ordre du programme ; à la place, « exécuter en premier l'instruction dont les données sont prêtes ».

Démo interactive : Simulateur de pipeline (style diagramme de Gantt)

Le code Python ci-dessous simule l'exécution d'instructions en modes pipeline et non pipeline, et affiche la sortie dans un style diagramme de Gantt, ce qui vous permet de comparer intuitivement les différences entre les deux approches.

Exemple

"""
Simulateur de pipeline CPU (exemple de démo)
Comparaison : non pipeline (série) vs pipeline (chevauchement parallèle)
Afficher la chronologie d'exécution des étapes de chaque instruction sous forme de diagramme de Gantt
"""


# Définir la séquence d'instructions et les abréviations pour chaque étape
INSTRUCTIONS = ["I1", "I2", "I3", "I4", "I5", "I6", "I7", "I8"]
STAGES = ["IF", "ID", "EX", "WB"]  # Récupération, décodage, exécution, écriture en retour
STAGE_NAMES = {"IF": "Récupération", "ID": "Décodage", "EX": "Exécution", "WB": "Écriture en retour"}


def simulate_serial(instructions, stages):
    """
Mode sans pipeline : une fois que chaque instruction a entièrement exécuté ses 4 étapes, la suivante commence.
Retour : (cycles totaux, tableau de chronologie 2D)
Format de chronologie : time_grid[cycle][stage_slot] = nom de l'instruction en cours d'exécution ou None
    """

    num_instr = len(instructions)
    num_stages = len(stages)
    total_cycles = num_instr * num_stages  # n instructions × 4 étapes

    # time_grid[cycle][stage_slot] → quelle instruction se trouve dans cette étape à ce cycle
    time_grid = [[None for _ in range(num_instr)] for _ in range(total_cycles)]

    for i, instr in enumerate(instructions):
        base_cycle = i * num_stages
        for s_idx, stage in enumerate(stages):
            cycle = base_cycle + s_idx
            time_grid[cycle][i] = f"{instr}({stage})"

    return total_cycles, time_grid


def simulate_pipeline(instructions, stages):
    """
Mode pipeline : différentes étapes de différentes instructions peuvent se chevaucher en exécution.
Retour : (cycles totaux, tableau de chronologie 2D)
    """

    num_instr = len(instructions)
    num_stages = len(stages)
    # Cycles de pipeline totaux = nombre d'étapes + (nombre d'instructions - 1)
    total_cycles = num_stages + (num_instr - 1)

    time_grid = [[None for _ in range(num_instr)] for _ in range(total_cycles)]

    for i, instr in enumerate(instructions):
        # L'étape s de l'instruction i s'exécute au cycle (i + s)
        for s_idx, stage in enumerate(stages):
            cycle = i + s_idx
            if cycle < total_cycles:
                time_grid[cycle][i] = f"{instr}({stage})"

    return total_cycles, time_grid


def print_gantt(instructions, stages, time_grid, total_cycles, mode_name):
    """Afficher la chronologie dans le style d'un diagramme de Gantt"""
    num_instr = len(instructions)

    # En-tête de tableau
    header = f{'cycle':>4}
    for instr in instructions:
        header += f" {instr:^10}"
    print(header)
    print("-" * (6 + 12 * num_instr))

    for cycle in range(total_cycles):
        row = f"{cycle+1:>4} "
        for i in range(num_instr):
            cell = time_grid[cycle][i]
            if cell:
                # Attribuer des étiquettes différentes aux différentes étapes
                if "(IF)" in cell:
                    row += f"  [\033[94mIF\033[0m]     "  # Bleu
                elif "(ID)" in cell:
                    row += f"  [\033[93mID\033[0m]     "  # Jaune
                elif "(EX)" in cell:
                    row += f"  [\033[92mEX\033[0m]     "  # Vert
                elif "(WB)" in cell:
                    row += f"  [\033[91mWB\033[0m]     "  # Rouge
            else:
                row += f"  {'':10}"
        print(row)

    print("-" * (6 + 12 * num_instr))
    print(fTotal des cycles : {total_cycles})
    return total_cycles


def print_simple_gantt(instructions, stages, time_grid, total_cycles, mode_name):
    Diagramme de Gantt simple (sans couleurs ANSI, compatible avec tous les terminaux)
    num_instr = len(instructions)

    print(f"\n{'='*70}")
    print(f" {mode_name}")
    print(f"{'='*70}")

    # En-tête de tableau
    header = f{'cycle':>5} |
    for instr in instructions:
        header += f" {instr:^9} |"
    print(header)
    print("-" * (7 + 11 * num_instr))

    for cycle in range(total_cycles):
        row = f" {cycle+1:>3} |"
        for i in range(num_instr):
            cell = time_grid[cycle][i]
            if cell:
                # Extraire les identifiants d'étapes
                for stage in stages:
                    if f"({stage})" in cell:
                        row += f" [{stage}] |"
                        break
            else:
                row += f"          |"
        print(row)

    print("-" * (7 + 11 * num_instr))
    print(fTotal des cycles : {total_cycles})
    return total_cycles


def calculate_metrics(serial_cycles, pipeline_cycles, num_instr, num_stages):
    """Calculer les métriques de performance"""
    speedup = serial_cycles / pipeline_cycles
    ideal_speedup = num_stages  # Accélération idéale = nombre d'étages du pipeline

    # Débit : nombre d'instructions terminées par cycle
    serial_throughput = num_instr / serial_cycles
    pipeline_throughput = num_instr / pipeline_cycles

    # Efficacité du pipeline
    # Nombre total d'emplacements occupés par toutes les instructions
    used_slots = num_instr * num_stages
    total_slots = num_instr * pipeline_cycles
    efficiency = (used_slots / total_slots) * 100 if total_slots > 0 else 0

    return {
        'speedup': speedup,
        'ideal_speedup': ideal_speedup,
        'serial_throughput': serial_throughput,
        'pipeline_throughput': pipeline_throughput,
        'efficiency': efficiency,
        'serial_cycles': serial_cycles,
        'pipeline_cycles': pipeline_cycles,
    }


# ===== Programme principal =====
print("=" * 70)
print(Simulateur de pipeline CPU — Comparaison de style de diagramme de Gantt (démo d'exemple))
print("=" * 70)
print(fSéquence d'instructions : {', '.join(INSTRUCTIONS[:8])})
print(fÉtapes du pipeline : 4 (IF=Recherche d'instruction, ID=Décodage d'instruction, EX=Exécution, WB=Écriture en retour))
print()

Sans pipeline (en série)
serial_cycles, serial_grid = simulate_serial(INSTRUCTIONS, STAGES)
print_simple_gantt(INSTRUCTIONS, STAGES, serial_grid, serial_cycles, "Mode A : Sans pipeline (exécution en série)")

# 2. Avec pipeline
pipeline_cycles, pipeline_grid = simulate_pipeline(INSTRUCTIONS, STAGES)
print_simple_gantt(INSTRUCTIONS, STAGES, pipeline_grid, pipeline_cycles, "Mode B : Avec pipeline (pipeline à 4 étages)")

# 3. Comparaison des performances
metrics = calculate_metrics(serial_cycles, pipeline_cycles, len(INSTRUCTIONS), len(STAGES))

print(f"\n{'='*70}")
print(f" Analyse comparative des performances")
print(f"{'='*70}")
print(fNombre d'instructions : {len(INSTRUCTIONS)})
print(fÉtages du pipeline : {len(STAGES)})
print(f"")
print(fCycles totaux sans pipeline : {metrics['serial_cycles']} cycles)
print(fCycles totaux avec pipeline : {metrics['pipeline_cycles']} cycles)
print(f"")
print(fAccélération réelle : {metrics['speedup']:.2f}x)
print(fAccélération idéale : {metrics['ideal_speedup']}x (égale au nombre d'étages du pipeline))
print(f"")
print(f" Débit série : {metrics['serial_throughput']:.3f} instructions/cycle")
print(f" Débit du pipeline : {metrics['pipeline_throughput']:.3f} instructions/cycle")
print(fEfficacité du pipeline : {metrics['efficiency']:.1f}%)
print(f"")
print(fConclusion : après avoir utilisé un pipeline à {len(STAGES)} étages, les mêmes 8 instructions)
print(f" de {metrics['serial_cycles']} cycles raccourci à {metrics['pipeline_cycles']} cycles")
print(fAccéléré de {metrics['speedup']:.1f} fois !)
print(f"")
print(fRemarque : le pipeline a un surcoût de « remplissage » et de « vidage ».)
print(fDans les {len(STAGES)-1} premiers cycles, le pipeline n'est pas plein (période de remplissage),)
print(fDans les {len(STAGES)-1} derniers cycles, le pipeline se vide progressivement.)
print(fPlus il y a d'instructions, plus la surcharge relative de remplissage/vidage est faible,)
print(fPlus le rapport d'accélération s'approche de la valeur idéale {len(STAGES)}x.)


# 4. Démonstration étendue : évolution du rapport d'accélération pour différents nombres d'instructions
print(f"\n{'='*70}")
print(f" Analyse étendue : rapport d'accélération pour différents nombres d'instructions")
print(f"{'='*70}")
print(f" {'Directivenumber':>6} | {'stringlineweekperiod':>8} | {'Pipelineweekperiod':>10} | {'Speedup':>8} | {'Speedup/manage想Value':>14}")
print(f" {'-'*6}-+-{'-'*8}-+-{'-'*10}-+-{'-'*8}-+-{'-'*14}")

for n in [1, 2, 4, 8, 16, 32, 64, 128, 256]:
    test_instrs = [f"I{i}" for i in range(1, n+1)]
    s_cycles, _ = simulate_serial(test_instrs, STAGES)
    p_cycles, _ = simulate_pipeline(test_instrs, STAGES)
    sp = s_cycles / p_cycles
    ideal = len(STAGES)
    ratio = (sp / ideal) * 100
    print(f" {n:>6} | {s_cycles:>8} | {p_cycles:>10} | {sp:>7.2f}x | {ratio:>13.1f}%")

print()
print(fConclusion : plus il y a d'instructions, plus l'accélération se rapproche de la valeur idéale {len(STAGES)}x (c'est-à-dire le nombre d'étages du pipeline).)
print(fLorsqu'il n'y a qu'une seule instruction, le pipeline n'a aucun effet d'accélération.)
print(fMais les programmes modernes contiennent généralement des millions d'instructions, ce qui rend l'efficacité du pipeline proche de 100 %.)

Exécutez le code ci-dessus, et vous verrez :

  • Comparaison des diagrammes de GanttObservation : En mode non pipeline, chaque instruction occupe 4 cycles consécutifs, avec de grands écarts d'inactivité entre les instructions. En mode pipeline, les étages sont étroitement agencés avec presque aucun cycle d'inactivité.
  • Tendance de l'accélérationObservation : Plus il y a d'instructions, plus l'accélération se rapproche de la profondeur du pipeline (ici, 4). Mais avec une seule instruction, le pipeline n'apporte aucune accélération, car les surcoûts de remplissage et de vidage représentent 100 %.
  • DébitObservation : Après que le pipeline est « rempli » (à partir du 4e cycle), une instruction est terminée à chaque cycle. C'est la valeur essentielle du pipeline.

Démo interactive : comparaison de diagrammes de Gantt de pipeline

Ci-dessous, nous utilisons un diagramme à barres empilées horizontales Chart.js pour tracer des diagrammes de Gantt, en comparant directementSans pipeline (série)etAvec pipeline (4 étages), la répartition temporelle de chaque étage pour 4 instructions dans les deux modes. Chaque bloc de couleur représente un étage (IF=extraction, ID=décodage, EX=exécution, WB=écriture en retour), et l'axe des x correspond aux cycles d'horloge.

Comparaison de diagrammes de Gantt de pipeline (exemple de démonstration)

IF extraction
ID décodage
EX exécution
WB écriture en retour

Sans pipeline (exécution en série) — 16 cycles au total

Avec pipeline (pipeline à 4 étages) — 7 cycles au total

Nombre total de cycles sans pipeline
16
Nombre total de cycles avec pipeline
7
Accélération
2.29x
Amélioration du débit
129%
Observation clé :
Mode non pipeline : chaque instruction occupe exclusivement 4 cycles consécutifs, les instructions sont complètement sérialisées, et 4 instructions prennent 16 cycles.
En mode pipeline : exécution en chevauchement des différents étages de différentes instructions, 4 instructions ne nécessitent que 7 cycles (= 4 + (4-1)).
Une fois le pipeline "rempli" (à partir du cycle 4), une instruction se termine à chaque cycle — c'est pourquoi le débit s'améliore considérablement.

Comparaison de la profondeur des étages de pipeline

La "profondeur" du pipeline détermine la limite supérieure théorique de l'accélération, mais plus le pipeline est profond, plus les problèmes d'aléas sont graves :

Modèle de CPUNombre d'étages de pipelineFonctionnalités
RISC classique (par ex. MIPS)5 niveauxSimple et efficace, adapté à l'enseignement. Pipeline classique à 5 étages : IF, ID, EX, MEM, WB
Intel Pentium 4(NetBurst)20~31 étagesPipeline ultra-profond, à la recherche de hautes fréquences. Mais les erreurs de prédiction de branche sont coûteuses, et il a finalement été abandonné.
Série Intel Core (depuis 2006)14~19 étagesPipeline plus court + IPC élevé, équilibrant fréquence et efficacité.
Apple M1(2020)8 étages (décodage)Architecture ultra-large (émission sur 8 voies), faible fréquence, IPC élevé, excellente efficacité énergétique.

On peut voir que la profondeur du pipeline est un compromis : trop d’étages rendent le coût d’une seule erreur de prédiction de branche trop élevé ; trop peu d’étages empêchent la fréquence de monter suffisamment haut.

La tendance dominante dans la conception des CPU modernes est« large et peu profond »— profondeur de pipeline modérée + largeur d'émission ultra-large (capable de commencer à exécuter plusieurs instructions par cycle), plutôt que de rechercher aveuglément davantage d'étages.

autres extensions