Pipeline: cómo hacer que la CPU sea más rápida

¿Puede la CPU volverse más rápida sin aumentar la frecuencia del reloj? La respuesta es sí. La tecnología de pipeline es uno de los métodos más importantes. Esta lección utiliza código Python y una salida visual en estilo diagrama de Gantt para mostrar cómo el pipeline mejora enormemente la eficiencia de ejecución de las instrucciones.


Analogía cotidiana: la línea de lavado de autos

Imagina que tienes un negocio de lavado de autos. Hay dos formas de operarlo:

Opción 1: Encargado único (sin pipeline)

Un trabajador se encarga de todo el proceso de lavado de un automóvil de principio a fin: enjuague, aplicación de espuma, cepillado y secado. Solo cuando termina con un auto puede comenzar con el siguiente. Cada auto tarda 40 minutos; en una hora solo se pueden lavar 1.5 autos.

Opción 2: Trabajo dividido en línea de producción (con pipeline)

4 trabajadores se encargan cada uno de una etapa: enjuague, espuma, cepillado y secado. Después de que el primer automóvil entra en la etapa de enjuague, el trabajador de enjuague puede comenzar a enjuagar el segundo, mientras el trabajador de espuma aplica espuma al primero. Los cuatro trabajan simultáneamente y un automóvil se completa cada 10 minutos.

La segunda opción no hace que cada etapa individual sea más rápida (el enjuague sigue tardando 10 minutos), pero al superponer las etapas de diferentes automóviles, la velocidad de producción general aumenta considerablemente.

Este es el concepto central del pipeline de la CPU: superponer la ejecución de las diferentes etapas de diferentes instrucciones.


Ejecución en serie vs. ejecución en pipeline

El problema de la ejecución en serie

Recordando la lección 12, cada instrucción debe pasar por cuatro etapas: búsqueda de instrucción (IF), decodificación (ID), ejecución (EX) y escritura de resultado (WB).

EnEjecución en serie (sin pipeline)En este modo, la CPU debe completar las cuatro etapas de la instrucción actual antes de buscar la siguiente.

周期:  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 个周期

Las ventajas de la ejecución en pipeline

EnEjecución en pipelineEn este modo, tan pronto como la instrucción I1 completa la etapa de búsqueda (IF), el componente de búsqueda de la CPU queda libre. En lugar de dejarlo esperando, comienza inmediatamente a buscar la siguiente instrucción I2.

De manera similar, cuando I1 completa la decodificación (ID) y entra en la etapa de ejecución, el componente de decodificación comienza a procesar I2, mientras el componente de búsqueda comienza con 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 个周期(比串行快了一倍!)

Observa los números clave:

  • Sin pipeline: n instrucciones = 4n ciclos
  • Con pipeline: n instrucciones = 4 + (n-1) = n + 3 ciclos (es decir, una vez que el pipeline está lleno, se completa una instrucción por ciclo).

Cuando n es grande, la aceleración del pipeline se aproxima a 4 (el número de etapas del pipeline).

El pipeline no hace que una instrucción individual se ejecute más rápido (cada instrucción sigue necesitando 4 ciclos), pero aumenta considerablemente el «rendimiento» (throughput): la cantidad de instrucciones completadas por unidad de tiempo.


El problema de los «hazards» (riesgos) del pipeline

El pipeline es bueno, pero no está exento de costos. Hay tres tipos típicos de «hazards» (riesgos) que pueden detener el pipeline:

Tipo de hazardInglésDescripción del problemaAnalogía cotidiana
Hazard estructuralStructural HazardDos etapas compiten por el mismo recurso de hardware (por ejemplo, ambas necesitan acceder a la memoria).En el lavadero solo hay una pistola de agua a presión; tanto el enjuague inicial como el lavado final necesitan usarla, por lo que hay que hacer cola.
Hazard de datosData HazardLa siguiente instrucción depende del resultado de la anterior, pero ese resultado aún no se ha escrito (write back).Para hacer cerdo estofado, primero hay que cortar la carne y luego ponerla en la sartén. Si el cortador de carne no ha terminado, el cocinero tiene que esperar.
Hazard de controlControl HazardCuando se encuentra una instrucción de salto, no se sabe cuál instrucción debe buscarse a continuación.El menú dice: «Si el cliente pide picante, usa otra receta». Hasta que el cliente decida, la cocina no sabe qué preparar.

Las CPU modernas usan diversas técnicas para mitigar estos hazards:

  • Reenvío de datos (Data Forwarding): no espera a que el resultado se escriba en el registro; toma «prestado» el dato directamente de la salida de la ALU para que la siguiente instrucción lo use.
  • Predicción de ramas (Branch Prediction): Adivina la dirección del salto y busca las instrucciones por adelantado. Si acierta, gana; si falla, se vacía el pipeline y se empieza de nuevo.
  • Ejecución fuera de orden (Out-of-Order Execution): No se ejecuta en el orden del programa, sino que «se ejecuta primero la instrucción cuyos datos estén listos».

Demostración interactiva: simulador de pipeline (estilo diagrama de Gantt)

El siguiente código Python simula el proceso de ejecución de instrucciones tanto con pipeline como sin pipeline, e imprime la salida en estilo diagrama de Gantt para que puedas comparar visualmente las diferencias entre ambos modos.

Ejemplo

"""
Simulador de pipeline de CPU (demostración de ejemplo)
Comparación: sin pipeline (serie) vs. con pipeline (superposición paralela)
Imprime la línea de tiempo de ejecución de cada etapa de cada instrucción en estilo diagrama de Gantt.
"""


# Define la secuencia de instrucciones y las abreviaturas de cada etapa
INSTRUCTIONS = ["I1", "I2", "I3", "I4", "I5", "I6", "I7", "I8"]
STAGES = ["IF", "ID", "EX", "WB"]  # Búsqueda, decodificación, ejecución, escritura
STAGE_NAMES = {"IF": "Búsqueda", "ID": "Decodificación", "EX": "Ejecución", "WB": "Escritura"}


def simulate_serial(instructions, stages):
    """
Modo sin pipeline: cada instrucción completa las 4 etapas antes de comenzar la siguiente.
Retorna: (número total de ciclos, matriz bidimensional de línea de tiempo)
Formato de la línea de tiempo: time_grid[ciclo][etapa] = nombre de la instrucción actualmente en ejecución o None
    """

    num_instr = len(instructions)
    num_stages = len(stages)
    total_cycles = num_instr * num_stages  # n instrucciones × 4 etapas

    # time_grid[cycle][stage_slot] → qué instrucción está en esta etapa en este ciclo
    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):
    """
Modo con pipeline: las diferentes etapas de diferentes instrucciones pueden ejecutarse superpuestas.
Retorna: (número total de ciclos, matriz bidimensional de línea de tiempo)
    """

    num_instr = len(instructions)
    num_stages = len(stages)
    # Ciclos totales del pipeline = número de etapas + (número de instrucciones - 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):
        # La etapa s de la instrucción i se ejecuta en el ciclo (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):
    """Imprime la línea de tiempo en estilo diagrama de Gantt"""
    num_instr = len(instructions)

    # Encabezado
    header = f"{'Ciclo':>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:
                # Asignar diferentes marcadores a cada etapa
                if "(IF)" in cell:
                    row += f"  [\033[94mIF\033[0m]     "  # Azul
                elif "(ID)" in cell:
                    row += f"  [\033[93mID\033[0m]     "  # Amarillo
                elif "(EX)" in cell:
                    row += f"  [\033[92mEX\033[0m]     "  # Verde
                elif "(WB)" in cell:
                    row += f"  [\033[91mWB\033[0m]     "  # Rojo
            else:
                row += f"  {'':10}"
        print(row)

    print("-" * (6 + 12 * num_instr))
    print(f"Ciclos totales: {total_cycles}")
    return total_cycles


def print_simple_gantt(instructions, stages, time_grid, total_cycles, mode_name):
    """Versión simple de diagrama de Gantt (sin colores ANSI, compatible con todas las terminales)"""
    num_instr = len(instructions)

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

    # Encabezado
    header = f"{'Ciclo':>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:
                # Extraer el marcador de etapa
                for stage in stages:
                    if f"({stage})" in cell:
                        row += f"  [{stage}]   |"
                        break
            else:
                row += f"          |"
        print(row)

    print("-" * (7 + 11 * num_instr))
    print(f" Ciclos totales: {total_cycles}")
    return total_cycles


def calculate_metrics(serial_cycles, pipeline_cycles, num_instr, num_stages):
    """Calcular métricas de rendimiento"""
    speedup = serial_cycles / pipeline_cycles
    ideal_speedup = num_stages  # Aceleración ideal = número de etapas del pipeline

    # Rendimiento (throughput): instrucciones completadas por ciclo
    serial_throughput = num_instr / serial_cycles
    pipeline_throughput = num_instr / pipeline_cycles

    # Eficiencia del pipeline
    # Número total de ranuras ocupadas por todas las instrucciones
    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,
    }


# ===== Programa principal =====
print("=" * 70)
print(" Simulador de pipeline de CPU — Comparación en estilo diagrama de Gantt (demostración de ejemplo)")
print("=" * 70)
print(f" Secuencia de instrucciones: {', '.join(INSTRUCTIONS[:8])}")
print(f" Etapas del pipeline: 4 (IF=buscar, ID=decodificar, EX=ejecutar, WB=escribir)")
print()

# 1. Sin pipeline (serie)
serial_cycles, serial_grid = simulate_serial(INSTRUCTIONS, STAGES)
print_simple_gantt(INSTRUCTIONS, STAGES, serial_grid, serial_cycles, "Modo A: sin pipeline (ejecución en serie)")

# 2. Con pipeline
pipeline_cycles, pipeline_grid = simulate_pipeline(INSTRUCTIONS, STAGES)
print_simple_gantt(INSTRUCTIONS, STAGES, pipeline_grid, pipeline_cycles, "Modo B: con pipeline (pipeline de 4 etapas)")

# 3. Comparación de rendimiento
metrics = calculate_metrics(serial_cycles, pipeline_cycles, len(INSTRUCTIONS), len(STAGES))

print(f"\n{'='*70}")
print(f" Análisis comparativo de rendimiento")
print(f"{'='*70}")
print(f" Número de instrucciones: {len(INSTRUCTIONS)}")
print(f" Etapas del pipeline: {len(STAGES)}")
print(f"")
print(f" Ciclos totales sin pipeline: {metrics['serial_cycles']}")
print(f" Ciclos totales con pipeline: {metrics['pipeline_cycles']}")
print(f"")
print(f" Aceleración real: {metrics['speedup']:.2f}x")
print(f" Aceleración ideal: {metrics['ideal_speedup']}x (igual al número de etapas del pipeline)")
print(f"")
print(f" Rendimiento en serie: {metrics['serial_throughput']:.3f} instrucciones/ciclo")
print(f" Rendimiento con pipeline: {metrics['pipeline_throughput']:.3f} instrucciones/ciclo")
print(f" Eficiencia del pipeline: {metrics['efficiency']:.1f}%")
print(f"")
print(f" Conclusión: tras usar un pipeline de {len(STAGES)} etapas, las mismas 8 instrucciones")
print(f" se reducen de {metrics['serial_cycles']} ciclos a {metrics['pipeline_cycles']} ciclos")
print(f" ¡una aceleración de {metrics['speedup']:.1f} veces!")
print(f"")
print(f" Nota: el pipeline tiene una sobrecarga de «llenado» y «vaciado».")
print(f" Durante los primeros {len(STAGES)-1} ciclos, el pipeline no está lleno (periodo de llenado),")
print(f" Durante los últimos {len(STAGES)-1} ciclos, el pipeline se vacía gradualmente.")
print(f" Cuantas más instrucciones, menor es la sobrecarga relativa de llenado/vaciado,")
print(f" y más se acerca el speedup al valor ideal {len(STAGES)}x.")


"# 4. Demostración ampliada: tendencia del speedup con diferentes números de instrucciones"
print(f"\n{'='*70}")
print(f" Análisis ampliado: speedup con diferentes números de instrucciones")
print(f"{'='*70}")
print(f" {'Instrucciones':>6} | {'Ciclos seriales':>8} | {'Ciclos pipeline':>10} | {'Speedup':>8} | {'Speedup/ideal':>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(f" Conclusión: cuantas más instrucciones, más se acerca el speedup al valor ideal {len(STAGES)}x (es decir, el número de etapas del pipeline).")
print(f" Cuando solo hay 1 instrucción, el pipeline no tiene ningún efecto de aceleración.")
print(f" Pero los programas modernos suelen contener millones de instrucciones, por lo que la eficiencia del pipeline se acerca al 100%.")

"Al ejecutar el código anterior, verás:"

  • "Comparación de diagramas de Gantt"": en el modo sin pipeline, cada instrucción ocupa 4 ciclos consecutivos y hay mucho espacio en blanco entre instrucciones. En el modo con pipeline, las etapas están estrechamente ordenadas y casi no hay ciclos vacíos."
  • "Tendencia del speedup"": cuantas más instrucciones, más se acerca el speedup al número de etapas del pipeline (aquí 4). Pero con una sola instrucción, el pipeline no tiene ningún efecto de aceleración, porque el coste del periodo de llenado y vaciado representa el 100%."
  • "Throughput"": cuando el pipeline está «lleno» (a partir del 4.º ciclo), puede completar una instrucción en cada ciclo. Este es el valor fundamental del pipeline."

Demostración interactiva: comparación de diagramas de Gantt de pipeline

"A continuación se usa Chart.js para dibujar un diagrama de Gantt con barras apiladas horizontales, comparando visualmente""Sin pipeline (serial)"y"Con pipeline (4 etapas)""La distribución temporal de las etapas de 4 instrucciones en ambos modos. Cada bloque de color representa una etapa (IF=fetch, ID=decode, EX=execute, WB=write-back), y el eje x es el ciclo de reloj."

Comparación de diagramas de Gantt de pipeline (demostración de ejemplo)

"IF: fetch"
"ID: decode"
"EX: execute"
"WB: write-back"

"Sin pipeline (ejecución serial) — 16 ciclos en total"

"Con pipeline (pipeline de 4 etapas) — 7 ciclos en total"

"Total de ciclos sin pipeline"
16
"Total de ciclos con pipeline"
7
"Speedup"
2.29x
"Mejora del throughput"
129%
"Observaciones clave:"
"Modo sin pipeline: cada instrucción ocupa 4 ciclos consecutivos, las instrucciones son completamente seriales y las 4 instrucciones tardan 16 ciclos."
"Modo con pipeline: las diferentes etapas de diferentes instrucciones se ejecutan superpuestas; las 4 instrucciones solo necesitan 7 ciclos (= 4 + (4-1))."
"Cuando el pipeline está «lleno» (a partir del ciclo 4), se completa una instrucción en cada ciclo: esta es la razón del gran aumento del throughput."

Comparación de la profundidad de diferentes pipelines

"El «número de etapas» del pipeline determina el límite teórico del speedup, pero cuantas más etapas, más graves son los problemas de riesgo:"

"Modelo de CPU""Etapas del pipeline""Características"
"RISC clásico (como MIPS)""5 etapas""Sencillo y eficiente, adecuado para la enseñanza. Pipeline clásico de cinco etapas: IF, ID, EX, MEM, WB"
Intel Pentium 4(NetBurst)"20~31 etapas""Pipeline ultralargo, orientado a altas frecuencias. Pero cuando falla la predicción de ramas, la pérdida es grave, por lo que finalmente se abandonó."
"Serie Intel Core (desde 2006)""14~19 etapas""Pipeline más corto + alto IPC, equilibra frecuencia y eficiencia"
Apple M1(2020)"8 etapas (decodificación)""Arquitectura ultraancha (emisión de 8), baja frecuencia y alto IPC, con excelente eficiencia energética."

"Se puede ver que la profundidad del pipeline es un equilibrio: demasiadas etapas hacen que el coste de un fallo de predicción de rama sea muy alto; muy pocas etapas impiden que la frecuencia suba."

"La tendencia principal en el diseño moderno de CPU es""«ancho y poco profundo»""— una profundidad de pipeline moderada + un ancho de emisión superancho (poder comenzar a ejecutar varias instrucciones en cada ciclo), en lugar de obsesionarse con el número de etapas."

"Otras extensiones"