σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyHojas de rutaRutas
Ctrl K
AO
+1,2 k estudiantes
Iniciar sesiónUnirse ↗
Hojas de rutaRutas
AO
+1,2 k estudiantes
Iniciar sesión
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyHojas de rutaRutas
Ctrl K
AO
+1,2 k estudiantes
Iniciar sesiónUnirse ↗
Hojas de rutaRutas
AO
+1,2 k estudiantes
Iniciar sesión
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Ruta de Cache locality›Módulo 3 · La máquina en conjunto›Tema 3.3·~55 minAvanzado

Falso compartir y relleno

Posición en la rutaMódulo 3 de 3 · Tema 3 de 7
3.3 Falso compartir y relleno
—Puntuación del lab— / 100XP total—Racha—

6 min de lectura3 ejemplos de código

  1. 01Los núcleos tienen sus propias cachés
  2. 02Un contador por núcleo
  3. 03Cuánto relleno
  4. 04Contadores dirigidos por fechas reales
  5. 05Lo que va a construir

Cuatro hilos actualizan cada uno su propio contador, no se comparte nada, y aun así el bucle corre 40 veces más lento de lo debido. La razón es que los cuatro contadores están en una misma línea de caché y los núcleos se la disputan. Esta lección construye un pequeño modelo de coherencia de caché, muestra lo que cuesta el falso compartir en un bucle dirigido por las fechas de la tabla de SPY y lo arregla con unos bytes de relleno.

01

Los núcleos tienen sus propias cachés

Hasta ahora un programa corría en un núcleo. Una máquina real tiene varios núcleos y cada uno tiene su propia caché pequeña y rápida. Esto crea un problema: si el núcleo 0 y el núcleo 1 tienen una copia de una línea y el núcleo 0 la cambia, el núcleo 1 no debe seguir leyendo el valor viejo. La regla de hardware que lo evita es la coherencia de caché. La forma más simple de imponerla: antes de que un núcleo escriba en una línea, se descarta la copia de esa línea en todos los demás núcleos. Eso es una invalidación.

  1. 1
    Mirar en su propia cachéSi la línea no está, este acceso es un fallo.
  2. 2
    Al escribir, expulsar a los demásCada otro núcleo que tenga la línea pierde su copia: una invalidación cada uno.
  3. 3
    Quedarse con la líneaDespués la línea está en la caché de este núcleo, lista para su próximo acceso.

La clase siguiente guarda, para cada núcleo, el conjunto de líneas que tiene. Repetimos tres pasos: el núcleo 0 escribe el byte 0, el núcleo 1 escribe el byte 8 (la misma línea) y luego el núcleo 0 escribe de nuevo el byte 0.

main.pyPython
class CoherentCaches:    def __init__(self, n_cores):        self.lines = [set() for _ in range(n_cores)]        self.misses = 0        self.invalidations = 0    def access(self, core, address, write):        line = address // LINE_BYTES        hit = line in self.lines[core]        if not hit:            self.misses += 1        if write:            for other, held in enumerate(self.lines):1                if other != core and line in held:                    held.discard(line)                    self.invalidations += 1        self.lines[core].add(line)        return hitcaches = CoherentCaches(2)print(caches.access(0, 0, True), caches.access(1, 8, True), caches.access(0, 0, True))print("misses:", caches.misses, "invalidations:", caches.invalidations)
False False False
misses: 3 invalidations: 2
  1. 1Una escritura quita la línea de cada otro núcleo que la tenga y cuenta una invalidación por copia.

Los tres accesos fallan. La escritura del núcleo 1 invalida la copia del núcleo 0, y la nueva escritura del núcleo 0 invalida a su vez la copia del núcleo 1. Los núcleos 0 y 1 nunca escribieron el mismo byte, solo bytes vecinos, y aun así se quitan la línea en cada escritura.

DefiniciónFalso compartir

Dos núcleos escriben variables distintas que están en la misma línea de caché. El programa es correcto, pero la línea rebota entre los núcleos como si compartieran una sola variable.

02

Un contador por núcleo

Una forma común de contar eventos en paralelo es dar a cada núcleo su propio contador, para que ningún núcleo espere a otro. Los contadores miden 8 bytes cada uno. Póngalos uno tras otro en un arreglo y cuatro caben en una línea de 64 bytes. Construimos las escrituras de 50 rondas de actualizaciones, una escritura por núcleo en cada ronda, y las repetimos con los contadores separados 8 bytes (compactos) y 64 bytes (con relleno). El tiempo es de 1 ns para un acceso en la caché del propio núcleo y de 60 ns para un fallo que trae la línea de otro núcleo.

main.pyPython
def counter_address(core, stride_bytes):    return core * stride_bytesdef update_trace(n_cores, n_updates, stride_bytes):    return [(core, "w", counter_address(core, stride_bytes)) for _ in range(n_updates) for core in range(n_cores)]def run_trace(trace, n_cores):    caches = CoherentCaches(n_cores)    for core, op, address in trace:        caches.access(core, address, op == "w")    return {"accesses": len(trace), "misses": caches.misses, "invalidations": caches.invalidations}def time_ns(stats):    return (stats["accesses"] - stats["misses"]) * HIT_NS + stats["misses"] * MISS_NSfor name, stride in (("packed", 8), ("padded", 64)):    stats = run_trace(update_trace(4, 50, stride), 4)    print(name, stats, time_ns(stats), "ns")
packed {'accesses': 200, 'misses': 200, 'invalidations': 199} 12000.0 ns
padded {'accesses': 200, 'misses': 4, 'invalidations': 0} 436.0 ns

Compactos, las 200 escrituras fallan y 199 invalidan al último escritor: 12 000 ns. Con relleno, cada núcleo guarda su contador en su propia línea: cuatro fallos en frío al principio, luego cada escritura acierta y no hay invalidaciones: 436 ns. Es 27 veces más rápido para exactamente la misma aritmética, porque los cuatro núcleos ya no se quitan la línea.

03

Cuánto relleno

Para dar a cada contador una línea propia, la distancia entre dos contadores debe ser múltiplo del tamaño de línea, así que el tamaño del elemento se redondea al siguiente múltiplo de 64. Un contador de 8 bytes ocupa 64 bytes, lo que desperdicia 56. Un elemento de 65 bytes ocupa 128. La función siguiente hace el redondeo.

main.pyPython
def padding_needed(item_bytes):    return math.ceil(item_bytes / LINE_BYTES) * LINE_BYTESprint([padding_needed(size) for size in (8, 63, 64, 65, 100)])
[64, 64, 64, 128, 128]
Cómo pedirlo en código real

En C++ escriba alignas(64) en la estructura que contiene el contador de un hilo (o std::hardware_destructive_interference_size), en Rust #[repr(align(64))], en Java @Contended. En Python con NumPy, haga el arreglo de contadores (n_cores, 8) y use solo la columna 0, de modo que cada contador empiece una nueva fila de 64 bytes.

Rellene solo lo que escriben los núcleos

Las lecturas solas nunca invalidan nada, así que muchos núcleos pueden leer una línea sin coste. El relleno desperdicia 56 bytes por contador de 8 bytes, un mal trato para datos solo leídos o para miles de elementos. Reserve el relleno para las pocas variables calientes por hilo que los hilos actualizan a menudo.

04

Contadores dirigidos por fechas reales

En el laboratorio cuatro núcleos cuentan cada uno los días bursátiles que atienden, y el día número i de la tabla de SPY va al núcleo día de la semana % 4. Los primeros 1000 días dirigen las escrituras en orden de fecha. Como los días siguen el calendario, el mismo núcleo rara vez escribe dos veces seguidas y la línea se quita una y otra vez. Con los contadores compactos, 818 de las 1000 escrituras fallan y 817 invalidan a otro núcleo; con ellos rellenados, solo fallan las cuatro primeras escrituras.

Qué dicen los datos reales1000 días bursátiles de SPY, 4 núcleos, 1 ns por acierto, 60 ns por fallo
Contadores compactos en una línea49,262ns818 fallos, 817 invalidaciones
Contadores con relleno a 64 bytes1,236ns4 fallos en frío, ninguna invalidación
Aceleración por el relleno39.9×el mismo trabajo, una línea cada uno
Memoria pagada a cambio56bytesdesperdiciados por contador de 8 bytes
Tiempo de 1000 actualizaciones, en microsegundos
Compactos49.3
Con relleno1.2
05

Lo que va a construir

El laboratorio construye el modelo de coherencia, las trazas de contadores por núcleo con un paso dado, la repetición, la regla de relleno, el modelo de tiempo y el estudio sobre las fechas de SPY.

  • Función · preguntaSe apoya en
  • CoherentCaches(n_cores).access(core, address, write)¿Acierto o fallo, y quién pierde una copia?Aritmética simple
  • counter_address(core, stride_bytes)¿Dónde vive el contador de un núcleo?Aritmética simple
  • update_trace(n_cores, n_updates, stride_bytes)¿Qué escrituras hacen los núcleos?counter_address
  • run_trace(trace, n_cores)¿Cuántos fallos e invalidaciones?CoherentCaches
  • padding_needed(item_bytes)¿A qué distancia deben estar los elementos?Aritmética simple
  • time_ns(stats)¿Cuánto tarda la ejecución?Los costes de acierto y fallo
  • study(bars)¿Cuánto cuesta el falso compartir con las fechas de SPY?run_tracepadding_neededtime_ns

Ideas clave

  • Una escritura toma la línea. Para escribir, un núcleo invalida la copia de la línea en todos los demás, que fallan la próxima vez que la usan.
  • Los vecinos comparten destino. Las variables de una misma línea de 64 bytes son una sola unidad para los núcleos: el falso compartir hace el código paralelo más lento que el serie.
  • Rellene los datos calientes por núcleo. Redondee cada elemento a 64 bytes: 40 veces más rápido aquí por 56 bytes desperdiciados por contador; no rellene datos solo leídos.
Comprobación rápida
Dos hilos incrementan cada uno su propio contador de 8 bytes, y los dos contadores son vecinos en un arreglo. ¿Qué pasa?
Elige una respuesta.

Siguiente tema: el falso compartir trata de núcleos que comparten un zócalo. En una máquina con dos zócalos la propia memoria está dividida: una parte está cerca de un núcleo y otra lejos. En Efectos NUMA coloca páginas en nodos, cuenta los accesos locales y remotos y valora la diferencia.

Lab: qué se califica

7 tareas · 100 pts
  • Un modelo de coherencia30 pts
  • La dirección de un contador5 pts
  • Rondas de actualizaciones15 pts
  • Repetir una ejecución15 pts
  • El relleno que da una línea10 pts
  • El tiempo de una ejecución10 pts
  • El estudio con la tabla de SPY15 pts
Explorador · lab-cl-9
▾ lab-cl-9
▾ data
…
bash · en el navegador
learner@quantcraft:~/lab-cl-9$
Python aún no cargadoPythonLn 1, Col 1Spaces: 4UTF-8

¿Restablecer este tema?

Todo lo que hay en lab-cl-9 vuelve a los archivos iniciales y se limpia el terminal. Tu solución actual se perderá: no se puede deshacer.

Se restablecerá
  • main.py → código inicial
  • Historial del terminal y salida de pruebas
  • Cambios sin enviar
Se conservará
  • Mejor puntuación del lab
  • Tu XP y tu racha
  • Progreso de la lección y notas