En un servidor de dos zócalos la misma lectura puede costar 80 ns o 130 ns, según el zócalo donde viva la página. Dónde vive una página se decide la primera vez que se toca. Esta lección modela dos nodos, muestra cómo un solo hilo que inicializa los datos hace remota la mitad del trabajo y lo arregla dejando que cada hilo toque primero su propia parte.
Una memoria con distancia
Un servidor grande tiene dos zócalos de procesador y cada zócalo tiene sus propios módulos de memoria. Un núcleo lee directamente la memoria de su zócalo. Para leer la del otro zócalo la petición cruza un enlace entre zócalos, lo que tarda más. Una máquina así es NUMA. Cada zócalo y su memoria forman un nodo, y el nodo de un hilo es donde está su núcleo. En nuestro modelo hay dos nodos con dos hilos cada uno: los hilos 0 y 1 en el nodo 0 y los hilos 2 y 3 en el nodo 1. Un acceso local cuesta 80 ns y uno remoto 130 ns.
print([node_of_thread(thread, 2) for thread in range(4)])for thread_node in (0, 1): print("thread on node", thread_node, [LOCAL_NS if page_node == thread_node else REMOTE_NS for page_node in (0, 1)])
[0, 0, 1, 1] thread on node 0 [80.0, 130.0] thread on node 1 [130.0, 80.0]
Cada fila es la vista de un hilo: paga 80 ns por las páginas de su propio nodo y 130 ns por las del otro. La diferencia, 50 ns, parece pequeña, pero se aplica a cada acceso que llega a la memoria.
Quién decide dónde vive una página
El sistema operativo da memoria a un programa en páginas y elige el nodo de una página en el momento en que se usa por primera vez. La regla por defecto es la primera toque: la página va al nodo del hilo que la toca primero y se queda allí. Una alternativa es intercalar las páginas entre los nodos. El código siguiente aplica ambas reglas a una lista corta de accesos escritos como (hilo, dirección).
def first_touch_placement(trace, threads_per_node): placement = {} for thread, address in trace: page = address // PAGE_BYTES if page not in placement: placement[page] = node_of_thread(thread, threads_per_node) return placementdef interleaved_placement(trace, n_nodes): return {page: page % n_nodes for page in {address // PAGE_BYTES for _, address in trace}}trace = [(0, 0), (0, 4096), (3, 8192), (1, 4100)]print(first_touch_placement(trace, 2))print(interleaved_placement(trace, 2))
{0: 0, 1: 0, 2: 1}
{0: 0, 1: 1, 2: 0}Las páginas 0 y 1 las tocó primero el hilo 0, que está en el nodo 0, así que van al nodo 0. La página 2 la tocó primero el hilo 3, en el nodo 1, así que va al nodo 1. El acceso posterior del hilo 1 a la página 1 no la mueve. El intercalado ignora quién toca qué: la página 1 va al nodo 1 simplemente porque 1 módulo 2 es 1.
Contar los accesos locales y remotos
Un trabajo paralelo típico divide una tabla en trozos, uno por hilo. Cada hilo lee luego solo su trozo, así que lo ideal es que cada página que lee esté en su propio nodo. Las funciones siguientes construyen ese patrón de acceso para 400 registros entre cuatro hilos, y cuentan los accesos locales y remotos y su tiempo para tres colocaciones. En la primera, «single», un solo hilo inicializó todos los datos, así que la primera toque puso cada página en el nodo 0. En la segunda, los propietarios inicializaron sus propios trozos. En la tercera, las páginas están intercaladas.
def access_costs(trace, placement, threads_per_node): local = 0 remote = 0 for thread, address in trace: if placement[address // PAGE_BYTES] == node_of_thread(thread, threads_per_node): local += 1 else: remote += 1 return {"local": local, "remote": remote, "ns": local * LOCAL_NS + remote * REMOTE_NS}def partition_trace(n_records, n_threads, record_bytes): chunk = n_records // n_threads return [(thread, record * record_bytes) for thread in range(n_threads) for record in range(thread * chunk, (thread + 1) * chunk)]work = partition_trace(400, 4, 512)single = first_touch_placement([(0, address) for _, address in work], 2)owners = first_touch_placement(work, 2)spread = interleaved_placement(work, 2)for name, placement in (("single", single), ("owners", owners), ("interleaved", spread)): print(name, access_costs(work, placement, 2))
single {'local': 200, 'remote': 200, 'ns': 42000.0}
owners {'local': 400, 'remote': 0, 'ns': 32000.0}
interleaved {'local': 208, 'remote': 192, 'ns': 41600.0}Con un solo hilo que inicializa todo, la mitad de los accesos son remotos y el trabajo tarda 42 000 ns para 400 lecturas. Cuando los propietarios inicializan sus trozos, nada es remoto y tarda 32 000 ns, unas 1,3 veces más rápido, para exactamente el mismo programa. El intercalado no ayuda aquí: como cada hilo lee un trozo contiguo, las páginas intercaladas ponen la mitad de sus datos en el nodo equivocado. El intercalado sirve para datos que todos los hilos leen por todas partes, donde ninguna colocación puede ser buena para todos.
Un hilo principal que reserva y llena un arreglo grande antes de lanzar los trabajadores decide la colocación de cada página, en su propio nodo. El programa es correcto y el perfilador no muestra nada raro, solo una ralentización que crece con el número de zócalos. Deje que cada trabajador inicialice la parte que va a usar.
En Linux, numactl --interleave=all reparte las páginas de un programa entre los nodos, numactl --cpunodebind y --membind fijan hilos y memoria a nodos, y numastat muestra cuántos accesos fueron locales. Los sistemas de trading suelen fijar cada hilo a un núcleo y darle memoria de su propio nodo.
Lo que va a construir
El laboratorio sigue la lección: el nodo de un hilo, las dos reglas de colocación, el patrón de acceso de un trabajo particionado, el recuento de accesos locales y remotos y el estudio sobre los registros de SPY.
- Función · preguntaSe apoya en
- node_of_thread(thread, threads_per_node)¿En qué nodo corre un hilo?La división entera
- first_touch_placement(trace, threads_per_node)¿Dónde pone la primera toque cada página?node_of_thread
- interleaved_placement(trace, n_nodes)¿Y el intercalado?Aritmética simple
- partition_trace(n_records, n_threads, record_bytes)¿Qué accesos hace cada hilo?Aritmética simple
- access_costs(trace, placement, threads_per_node)¿Cuántos accesos locales y remotos, y cuánto tiempo?node_of_thread
- study(bars)¿Cuánto cuesta una mala colocación en los registros de SPY?partition_tracefirst_touch_placementaccess_costs
Siguiente tema: dónde viven los bytes es solo la mitad de la historia; cómo se reparten también importa. En Asignadores y pools de memoria un programa pide muchos objetos pequeños, y el orden y los lugares en que el asignador los reparte deciden cuántas líneas de caché toca un recorrido posterior.
Lab: qué se califica
6 tareas · 100 pts- El nodo de un hilo5 pts
- Colocación por primera toque20 pts
- Colocación intercalada10 pts
- Un trabajo particionado15 pts
- Accesos locales y remotos25 pts
- El estudio con la tabla de SPY25 pts