Un procesador suma dos números en una fracción de nanosegundo, pero traer un número que no está en ninguna caché puede tardar 80 ns. Esta lección convierte esas cifras de latencia en una sola, el coste medio de un acceso a memoria, y muestra qué nivel de memoria se come su tiempo.
Por qué la memoria, y no la aritmética, marca la velocidad
Un programa de trading pasa la mayor parte de su vida esperando datos: un precio, una orden, una fila de una tabla. La aritmética sobre esos datos es casi gratis, pero los datos primero deben viajar de la memoria al procesador, y cuanto más lejos están, más tarda. Piense en un escritorio, una estantería, la planta de una biblioteca y un almacén al otro lado de la ciudad. Los pocos papeles del escritorio son instantáneos, la estantería tarda segundos, la biblioteca un minuto y el almacén una tarde. Los ordenadores copian los datos que más usan en memorias pequeñas y rápidas junto al procesador, llamadas cachés, para que la mayoría de las peticiones se atiendan en el escritorio.
El tiempo que pasa entre pedir un valor y tenerlo, medido aquí en nanosegundos (milmillonésimas de segundo).
Un núcleo de servidor típico tiene tres cachés delante de la memoria principal. Las llamamos L1, L2 y L3, y a la memoria principal RAM. Una petición atendida por una caché es un acierto; una petición que debe pasar al siguiente nivel es un fallo. La tabla siguiente pone las cuatro latencias una junto a otra.
LATENCY_NS = {"L1": 1.0, "L2": 4.0, "L3": 15.0, "RAM": 80.0}for level, ns in LATENCY_NS.items(): print(level, ns, "ns", ns / LATENCY_NS["L1"], "x L1")
L1 1.0 ns 1.0 x L1 L2 4.0 ns 4.0 x L1 L3 15.0 ns 15.0 x L1 RAM 80.0 ns 80.0 x L1
Cada nivel es varias veces más lento que el de arriba, y la RAM es 80 veces más lenta que L1. Toda esta habilidad consiste en lograr que la mayoría de los accesos se atiendan cerca de la cima.
Cuánto cuesta un acceso
Un acceso empieza en L1. Si el valor está allí, el acceso cuesta la latencia de L1 y termina. Si no, el procesador pregunta a L2, luego a L3 y luego a la RAM, que siempre tiene el valor. Cada nivel consultado cuesta tiempo, acierte o no, así que un acceso encontrado en L3 ha pagado L1, L2 y L3.
- 1Preguntar a L1Pague 1 ns. Un acierto termina el acceso.
- 2Si falla, preguntar a L2Pague 4 ns más. Un acierto termina el acceso.
- 3Si falla, preguntar a L3Pague 15 ns más. Un acierto termina el acceso.
- 4Si falla, ir a la RAMPague 80 ns más. La RAM siempre tiene el valor.
LATENCY_NS = {"L1": 1.0, "L2": 4.0, "L3": 15.0, "RAM": 80.0}cost_found_in_l3 = LATENCY_NS["L1"] + LATENCY_NS["L2"] + LATENCY_NS["L3"]print(cost_found_in_l3, "ns")
20.0 ns
Un acceso atendido por L3 cuesta 20 ns, no 15: el 1 ns y los 4 ns gastados en los dos fallos anteriores forman parte de la cuenta.
Cada tasa de acierto cuenta solo los accesos que llegan a ese nivel. Una tasa de acierto de L2 de 0,8 significa el 80 % de los accesos que fallaron en L1, no el 80 % de todos. Confundir ambas es el error más común de este tema.
El tiempo de acceso medio
Ningún acceso es típico, así que pedimos el promedio de muchos accesos. Pondere el coste de cada camino por su frecuencia. Todo acceso paga la latencia de L1; solo la parte que falla en L1 paga L2; solo la que falla en ambas paga L3; y así sucesivamente. Escrito como una expresión, con paréntesis que se anidan como los niveles:
Este es el tiempo de acceso medio a memoria. Lea los paréntesis de dentro hacia fuera: el más interno es lo que cuesta un fallo en L3, el siguiente suma la visita a L3, y así hasta la visita a L1 que paga todo acceso.
LATENCY_NS = {"L1": 1.0, "L2": 4.0, "L3": 15.0, "RAM": 80.0}h1, h2, h3 = 0.9, 0.8, 0.7t = LATENCY_NSamat = t["L1"] + (1 - h1) * (t["L2"] + (1 - h2) * (t["L3"] + (1 - h3) * t["RAM"]))1print(round(amat, 2), "ns")
2.18 ns
- 1
1 - hes la parte de los accesos que fallan, así que cada paréntesis se multiplica por la parte que llega hasta allí.
Con el 90 % de los accesos acertando en L1, el promedio sigue siendo 2,18 ns, más del doble de la latencia de L1. Los pocos accesos que bajan hasta el fondo son tan caros que dominan el promedio. Por eso una tasa de acierto que parece excelente puede esconder un programa lento.
Los fabricantes dan la latencia en ciclos del procesador: 4 ciclos a 4 GHz son 1 ns. Usamos nanosegundos para comparar los niveles de un vistazo. Estas cifras son típicas, no exactas: los chips reales difieren en un factor de dos en un sentido u otro.
Quién responde y quién cuesta
Dos preguntas parecen iguales pero tienen respuestas distintas. ¿Qué nivel atiende los accesos? ¿Y qué nivel se lleva el tiempo? La primera cuenta accesos, la segunda cuenta espera. A un nivel llega una parte de los accesos y cada visita cuesta su latencia, así que un nivel al que se llega rara vez pero que es lento puede dominar la espera.
h1, h2, h3 = 0.9, 0.8, 0.7served = [h1, (1 - h1) * h2, (1 - h1) * (1 - h2) * h3, (1 - h1) * (1 - h2) * (1 - h3)]reach = [1.0, 1 - h1, (1 - h1) * (1 - h2), (1 - h1) * (1 - h2) * (1 - h3)]1cost = [r * ns for r, ns in zip(reach, [1.0, 4.0, 15.0, 80.0])]2print([round(x, 3) for x in served])print([round(c / sum(cost), 3) for c in cost])
[0.9, 0.08, 0.014, 0.006] [0.459, 0.183, 0.138, 0.22]
- 1
reaches la parte de accesos que visitan cada nivel: todos visitan L1. - 2
sum(cost)es el tiempo de acceso medio: los mismos 2,18 ns de antes.
La RAM atiende solo el 0,6 % de los accesos pero supone el 22 % del tiempo de espera, mientras que L1 atiende el 90 % y se lleva el 46 % del tiempo. Este es el hecho clave detrás de toda optimización de esta habilidad: no cuente accesos, cuente adónde va el tiempo.
Leer una tabla real
¿Cuánto importa esto en una tabla real? La tabla de barras diarias de SPY del laboratorio tiene 5405 filas y cinco columnas (apertura, máximo, mínimo, cierre, volumen), es decir, 27 025 valores. Lea cada valor una vez en tres condiciones: cachés calientes donde casi todo acierta, cachés templadas y cachés frías donde la mitad de los accesos fallan en cada nivel.
Lo que va a construir
El laboratorio convierte esta lección en seis funciones pequeñas, cada una construida sobre las anteriores, y termina valorando la lectura de la tabla real de SPY. Aquí están con la pregunta que responde cada una.
- Función · preguntaSe apoya en
- average_access_time(hit_rates)¿Cuánto cuesta un acceso en promedio?La tabla de latencias
- slowdown(hit_rates)¿Cuántas veces más lento que acertar siempre en L1?average_access_time
- levels_served(hit_rates)¿Qué nivel atiende los accesos?Tasas de acierto locales
- time_shares(hit_rates)¿Qué nivel se lleva el tiempo?average_access_time
- scan_time_us(n_accesses, hit_rates)¿Cuánto tardan n accesos?average_access_time
- study(bars)¿Cuánto cuesta leer toda la tabla de SPY?scan_time_ustime_shares
Siguiente tema: esta lección tomó las tasas de acierto como dadas. En Líneas de caché y localidad espacial construye la caché que las produce y descubre por qué leer un valor junto a otro recién leído es casi gratis.
Lab: qué se califica
6 tareas · 100 pts- Tiempo de acceso medio20 pts
- Ralentización frente a L110 pts
- Quién atiende los accesos15 pts
- Quién se lleva el tiempo20 pts
- El tiempo de n accesos15 pts
- El estudio con la tabla de SPY20 pts