Práctica: Rendimiento del Sistema de Memoria
 
 

-- Ingeniería de Computadores --

OBJETIVO:
Analizar las prestaciones del sistema de memoria de un computador mediante el diseño e implementación de benchmarks sencillos.
DESCRIPCIÓN:

En esta práctica se diseñarán benchmarks que permitan medir las prestaciones de un sistema de memoria. Se analizará cómo el rendimiento depende fuertemente del empleo de la memoria cache y del ``stride'' de acceso a memoria.

Se diseñarán dos benchmarks, mem1d y mem2d, que realizarán un gran número de operaciones enteras sobre estructuras de datos unidimensionales y bidimensionales, respectivamente. Los benchmarks permitirán el análisis de los efectos del stride en el acceso a la memoria.

El rendimiento se medirá en términos de MOPS (millones de operaciones por segundo), una medida relativa de cuántas operaciones enteras sencillas se pueden efectuar por segundo cuando se accede a la estructura de datos según diversas formas.

DESARROLLO DE LA PRÁCTICA

Parte A: Benchmark MEM1D
1.- Diseño e implementación del benchmark mem1d.

El benchmark mem1d ha de medir el tiempo requerido para efectuar un gran número de sumas enteras de 32 bits sobre un array unidimensional. El benchmark tiene que realizar un conjunto de tests con diferente tamaño del problema. En algunos tests el tamaño empleado será tal que todos los datos se podrán localizar en la cache, mientras que en otros tests el tamaño del problema será tan grande que se requerirá otro nivel de cache o la memoria principal. Consecuentemente, se podrá apreciar cómo varía el ancho de banda de acceso a memoria, e incluso se podrá deducir el tamaño de la cache a partir de los resultados obtenidos.

Implementa en lenguaje C el benchmark mem1d según el siguiente algoritmo:
 

\begin{algorithm}% latex2html id marker 43\caption{Algoritmo del benchmark mem1......n\ENDFOR\STATE liberar\_memoria\_del\_array\end{algorithmic}\end{algorithm}

Los resultados que ha de proporcionar el benchmark para cada iteración son:

Tamaño del problema: $\Rightarrow$ loop_size * sizeof(int) bytes
Wall-time: $\Rightarrow$ Tiempo real que se tardó.
User-time: $\Rightarrow$ Tiempo de CPU de usuario.
Sys-time: $\Rightarrow$ Tiempo de CPU de sistema.
CPU-time: $\Rightarrow$ Tiempo de CPU total (usuario + sistema).
CPU/Wall: $\Rightarrow$ Relación CPU-time frente Wall-time: da una idea de la carga que
    tenía el sistema cuando se ejecutó el código.
MOPS : $\Rightarrow$ Millones de operaciones por segundo, medido en base al Wall-time.
2.- Compilación.

Compila el benchmark con el gcc, sin emplear flags de optimización:

gcc -o mem1d mem1d.c

3.- Ejecución del benchmark mem1d y análisis de los resultados.

4.- Nueva compilación y ejecución del benchmark mem1d.

Compila el benchmark de nuevo, esta vez habilitando el flag de optimización -O2:

gcc -O2 -o mem1d_opt mem1d.c

y repite el paso 3 con este nuevo programa. ¿Han mejorado los resultados sustancialmente?, ¿cuál crees que puede ser la razón?:

Parte B: Benchmark MEM2D
1.- Diseño e implementación del benchmark mem2d.

El benchmark mem2d ha de medir el tiempo requerido para efectuar un gran número de sumas enteras de 32 bits sobre un array bidimensional. Las operaciones se efectuarán siguiendo dos órdenes de recorrido distintos: En primer lugar se recorrerá el array siguiendo un orden por filas, lo cual se traduce en un acceso non-unit stride. Después se efectuarán las operaciones siguiendo un recorrido por columnas, lo cual equivale a un acceso de stride unidad. En los dos recorridos, el número de filas del array permanecerá constante, mientras que el número de columnas se va incrementando con las iteraciones. Los resultados permitirán analizar, en términos de MOPS, el ancho de banda de memoria con accesos con stride unidad y no-unidad.

Implementa en lenguaje C el benchmark mem2d según el siguiente algoritmo:
 

\begin{algorithm}% latex2html id marker 87\caption{Algoritmo del benchmark mem2......del tamaņo de la siguiente pasada. */\ENDWHILE\end{algorithmic}\end{algorithm}

Los resultados que se tienen que presentar son los mismos que en el caso del benchmark mem1d.

2.- Compilación.

Compila el benchmark con el gcc, sin emplear flags de optimización.

gcc -o mem2d mem2d.c

3. Ejecución y análisis de los resultados.

Al igual que se realizó con el mem1d, ejecuta el benchmark mem2d en varios computadores y compara los resultados. El principal efecto que se ha de observar es que el acceso unit-stride proporciona un número mayor de MOPS, que se incrementa conforme aumenta el tamaño del problema, mientras que el acceso non-unit stride proporciona un número bastante bajo de MOPS, que se decrementan con el tamaño del problema. Al igual que se realizó con el mem1d, crea una gráfica MOPS-tamaño del problema que muestre el conjunto de los resultados.

A modo de ejemplo se muestra una gráfica que compara el rendimiento obtenido con un Pentium Pro y un UltraSPARC para diversos accesos a memoria (con stride unidad y no-unidad,
ignórese las curvas etiquetadas con Interchangeable). Se puede comprobar cómo el acceso por stride unidad proporciona un rendimiento bastante superior al alcanzado en los accesos non-unit stride, en los cuales el efecto del cache thrashing es devastador.



4.- Nueva compilación y ejecución del benchmark mem2d.

Compila el benchmark de nuevo, habilitando el flag de optimización -O2:

gcc -O2 -o mem2d_opt mem2d.c

y repite el paso 3 con este nuevo programa. ¿Han mejorado los resultados sustancialmente?, ¿cuál crees que puede ser la razón?: