Salta al contenuto
Note per Studenti Esercizio - Località dei riferimenti, scorrere una matrice per righe o per colonne (laboratorio 7)

Esercizio - Località dei riferimenti, scorrere una matrice per righe o per colonne (laboratorio 7)

In questa pagina 4

Testo (laboratorio 7 del corso di Sistemi Operativi, Ing. Informatica UniPD).

  1. Sistema con pagine da 1024 byte e un solo frame, byte data[20][1024] (20 pagine). Quanti page fault fanno i due programmi che azzerano la matrice, uno con for i … for j … e l'altro con for j … for i …?
  2. Task 1. I file pagetest1.c (scorre per righe) e pagetest2.c (scorre per colonne) riempiono una matrice di interi di 1024 righe per 1024 colonne con rand(). Si compilano, si misurano con ./memtest ./pagetest1 (script che esegue perf stat -e cache-misses,dTLB-load-misses,dTLB-store-misses,page-faults) e si confrontano cache miss, TLB miss e page fault. Sono diversi?
  3. Task 2. Ripetere la misura per ROWS = 1024 e COLS = 256, 512, 1024, 2048, 4096, con 10 ripetizioni per combinazione, e calcolare la media dei cache miss.

Teoria: Memoria virtuale - paginazione su richiesta e sostituzione delle pagineLa memoria virtuale separa la memoria logica da quella fisica: basta che alcune pagine siano in RAM; con la paginazione su richiesta (demand paging) una pagina si carica solo al primo riferimento, e un riferimento a una pagina non residente (bit di validità = i) genera un page fault gestito dal sistema operativo in sei passi; EAT = (1 - p) * accesso + p * servizio del fault, con p minuscolo; copy-on-write rende economica la fork; se non ci sono frame liberi si sceglie una vittima con FIFO (anomalia di Belady), OPT (ottimo, irrealizzabile), LRU (algoritmo a stack), second chance (bit di riferimento, orologio) o la versione con bit di modifica; i frame si assegnano in modo uguale o proporzionale, con sostituzione globale o locale; se un processo non ha abbastanza frame va in thrashing; il modello del working set e la frequenza dei page fault lo evitano; considerazioni su prepaging, dimensione della pagina, TLB reach e struttura dei programmi; memoria del kernel con buddy system e slab.Memoria virtuale - paginazione su richiesta e sostituzione delle pagine →.


Parte 1: i fault con un solo frame

data[20][1024] di byte, con righe da 1024 byte, occupa 20 pagine da 1024 byte: ogni riga è esattamente una pagina. Il C salva le matrici per righe: data[0][0], data[0][1], …, data[0][1023], data[1][0], ….

Programma 1 (for i … for j …): scorre in ordine di indirizzo. Tutta la riga ii sta nella pagina ii: si riferisce la pagina 0 per 1024 volte, poi la 1 per 1024 volte, e così via. Con un solo frame: un fault a ogni cambio di pagina,

20 page fault.20\ \text{page fault}.

Programma 2 (for j … for i …): per ogni colonna jj si visitano le 20 righe, cioè le pagine 0,1,…,190,1,\ldots,19, e poi si torna alla pagina 0 per la colonna successiva. Con un solo frame ogni accesso cambia pagina: un fault a ogni scrittura,

1024⋅20=20 480 page fault.1024\cdot20=20\,480\ \text{page fault}.

Lo stesso lavoro, con una differenza di 1024 volte: dipende solo dall'ordine dei cicli (dalla località dei riferimenti).

Parte 2: i due programmi del laboratorio

Con la RAM reale e senza vincolo di un frame, il sistema ha abbastanza memoria: ogni pagina viene caricata (con un page fault) la prima volta che la si tocca e poi resta. La matrice di interi 1024×10241024\times1024 occupa 1024⋅1024⋅4=4 MB1024\cdot1024\cdot4=4\text{ MB}, cioè 4 MB/4 KB=10244\,\text{MB}/4\,\text{KB}=1024 pagine da 4 KB. Quindi:

contatore scorrimento per righe (pagetest1) per colonne (pagetest2)
page fault circa 1024 (una per pagina; con RAM sufficiente è uguale nei due casi) circa 1024 (uguale)
cache miss pochi: gli accessi sono consecutivi, una riga di cache da 64 B contiene 16 interi, quindi circa 1 048 576/16=65 5361\,048\,576/16=65\,536 miss di primo accesso molti di più: ogni accesso salta di 1024⋅4=40961024\cdot4=4096 byte, in una riga di cache diversa, e le righe di cache usate dagli altri accessi vengono sostituite prima di essere riutilizzate
dTLB miss circa uno per pagina (1024) molti di più: ogni accesso cade in una pagina diversa e le 1024 pagine non stanno nelle poche decine o centinaia di righe del TLB

Il commento della slide: con il sistema che assegna abbastanza frame, i TLB e le cache sono "precaricati", quindi il numero di page fault non cambia, cambiano il numero di TLB miss e di cache miss. In generale, il tempo di esecuzione di pagetest2 è molto maggiore di quello di pagetest1.

Il programma completo (in versione con la matrice come variabile statica, per evitare il limite dello stack da 8 MB; in pagetest1.c del corso la matrice è locale: per questo serve ulimit -s unlimited):

c
/* localita.c - scorrere una matrice per righe o per colonne.
 * Compilare:  gcc -O0 -Wall -Wextra -o localita localita.c
 * Usare:      ./localita righe     oppure     ./localita colonne
 * Misurare:   perf stat -e cache-misses,dTLB-load-misses,dTLB-store-misses,page-faults ./localita righe
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define RIGHE 1024
#define COLONNE 1024

static int dati[RIGHE][COLONNE]; /* 4 MB, nei dati statici: niente problema con lo stack da 8 MB */

int main(int argc, char **argv) {
    if (argc != 2) { fprintf(stderr, "uso: %s righe|colonne\n", argv[0]); return 1; }
    srand(1);
    if (strcmp(argv[1], "righe") == 0) {
        for (int i = 0; i < RIGHE; i++)           /* indirizzi consecutivi: 1 pagina ogni 1024 scritture */
            for (int j = 0; j < COLONNE; j++)
                dati[i][j] = rand();
    } else {
        for (int j = 0; j < COLONNE; j++)         /* salto di 4096 byte a ogni scrittura */
            for (int i = 0; i < RIGHE; i++)
                dati[i][j] = rand();
    }
    printf("%d\n", dati[RIGHE / 2][COLONNE / 2]); /* evita che il compilatore elimini il ciclo */
    return 0;
}

(Compilato qui con zig cc per Linux, senza avvisi; non eseguito: le misure con perf richiedono una macchina Linux, per cui i valori della tabella sono stime ragionate, non misure.)

Parte 3: variare le colonne

Con ROWS = 1024 e COLS ∈{256,512,1024,2048,4096}\in\{256,512,1024,2048,4096\} la matrice è di 4⋅1024⋅COLS4\cdot1024\cdot\text{COLS} byte: 1, 2, 4, 8, 16 MB, cioè 256, 512, 1024, 2048, 4096 pagine. I page fault crescono in modo proporzionale (uno per pagina). I cache miss:

  • scorrendo per righe crescono linearmente con ROWS⋅COLS/16\text{ROWS}\cdot\text{COLS}/16 (un miss ogni 16 interi consecutivi): è il minimo possibile;
  • scorrendo per colonne, il salto fra due accessi è di 4⋅COLS4\cdot\text{COLS} byte: quando le righe di cache toccate in una colonna (una per riga della matrice, 1024 righe di cache =64=64 KB) non stanno nella cache, ogni accesso è un miss, e il numero cresce come ROWS⋅COLS\text{ROWS}\cdot\text{COLS}, fino a 16 volte più dei cache miss per righe.

Procedura per la parte di misura: compilare ogni coppia (ROWS,COLS\text{ROWS},\text{COLS}), eseguire 10 volte perf stat, calcolare la media dei cache-misses e fare il grafico (COLS in ascissa, media in ordinata, una curva per ciascun programma). Si può automatizzare con uno script di shell che ripete l'esecuzione e usa perf stat -x, per estrarre il valore. Il grafico atteso: la curva "righe" cresce con pendenza piccola, quella "colonne" con pendenza molto maggiore.


Errori tipici

  • Dire che i page fault cambiano molto fra i due programmi quando c'è RAM sufficiente: cambiano TLB e cache.
  • Credere che il C salvi le matrici per colonne (è per righe, row-major).
  • Confondere pagina del sistema operativo (4096 byte) con riga di cache (64 byte).
  • Calcolare i fault con un frame senza accorgersi che ogni riga è una pagina (1024 byte).
  • Dimenticare ulimit -s unlimited quando la matrice da 4 MB è una variabile locale.

Versione ripasso

Un frame, byte data[20][1024], pagine da 1024 byte. Per righe (i esterno): 20 fault. Per colonne (j esterno): 1024⋅20=20 4801024\cdot20=20\,480 fault. Il C salva per righe (row-major): una riga è una pagina.

Matrice 1024×10241024\times1024 di interi (4 MB =1024=1024 pagine da 4 KB), RAM sufficiente. Page fault: circa 1024 in entrambi i programmi. Cache miss: per righe circa 1 048 576/16=65 5361\,048\,576/16=65\,536 (riga di cache da 64 B); per colonne molti di più (salto di 4096 B). TLB miss: per righe circa 1024; per colonne molti di più. Il numero di page fault non cambia, cambiano TLB e cache.

Varianti di COLS (256 ... 4096): pagine =COLS=\text{COLS} (con ROWS =1024=1024); cache miss per righe ∝ROWS⋅COLS/16\propto\text{ROWS}\cdot\text{COLS}/16, per colonne fino a 16 volte tanto. Media su 10 esecuzioni con perf stat; grafico COLS contro cache miss.

Numeri da ricordare. Matrice 1024×10241024\times1024 di int: 4 MB =1024=1024 pagine da 4 KB; 1 048 576 scritture; riga di cache da 64 B =16=16 interi →\to circa 65 536 miss per righe; per colonne salto di 4096 B a ogni scrittura. Misura: ./memtest ./pagetest1 (ulimit -s unlimited, perf stat -e cache-misses,dTLB-load-misses,dTLB-store-misses,page-faults). Con COLS =256…4096=256\ldots4096 (ROWS =1024=1024): 1, 2, 4, 8, 16 MB →\to COLS pagine; media di 10 esecuzioni; grafico COLS contro cache miss.

Errori tipici: page fault diversi con RAM sufficiente; matrici per colonne; pagina (4096) e riga di cache (64) scambiate; ulimit -s dimenticato.

Teoria: Memoria virtuale - paginazione su richiesta e sostituzione delle pagineLa memoria virtuale separa la memoria logica da quella fisica: basta che alcune pagine siano in RAM; con la paginazione su richiesta (demand paging) una pagina si carica solo al primo riferimento, e un riferimento a una pagina non residente (bit di validità = i) genera un page fault gestito dal sistema operativo in sei passi; EAT = (1 - p) * accesso + p * servizio del fault, con p minuscolo; copy-on-write rende economica la fork; se non ci sono frame liberi si sceglie una vittima con FIFO (anomalia di Belady), OPT (ottimo, irrealizzabile), LRU (algoritmo a stack), second chance (bit di riferimento, orologio) o la versione con bit di modifica; i frame si assegnano in modo uguale o proporzionale, con sostituzione globale o locale; se un processo non ha abbastanza frame va in thrashing; il modello del working set e la frequenza dei page fault lo evitano; considerazioni su prepaging, dimensione della pagina, TLB reach e struttura dei programmi; memoria del kernel con buddy system e slab.Memoria virtuale - paginazione su richiesta e sostituzione delle pagine →.

Teoria collegata