¿Cómo podemos usar caché (s) para la optimización de la multiplicación de matrices?

Comenzaré con el algoritmo básico y lo mejoraré para usar el caché.
Aquí A es una matriz (N, K), B es una matriz (K, M) y C = AB es una matriz (N, M).

  para i en rango (0, N):
   para j en el rango (0, M):
     para k en el rango (0, K):
       C [i] [j] + = A [i] [k] * B [k] [j]

(tenga en cuenta que este es el código de Python, por lo que el rango (0, N) contiene todos los enteros entre 0 y N-1. Si desea codificarlo en Python, le recomiendo que use matrices numpy para A, B y C)

Al hacer esto, su código pasará mucho tiempo mirando A [i] [k] y B [k] [j] en la memoria porque cambian en cada paso. Entonces hacemos 2 * N * K * M accesos de memoria.
La primera mejora que se debe hacer es invertir la suma en k, y la suma en j, para que podamos mantener el mismo r = A [i] [k] durante algún tiempo:

  para i en rango (0, N):
   para k en el rango (0, K):
     r = A [i] [k]
     para j en el rango (0, M):
       C [i] [j] + = r * B [k] [j]

Este método realmente no usa caché, pero la apuesta solo hace acceso a la memoria N * K + N * K * M. Ahora suponga que B es una matriz pequeña que puede caber en su efectivo. Luego, su computadora leerá una primera vez B durante el primer ciclo i = 1 y la almacenará en su caché. Entonces el acceso a B [k] [j] será muy rápido porque ya estará en caché. Entonces solo hará N * K + K * M accesos a memoria (y (N-1) * K * M accesos a caché)

Entonces, lo que hacemos es dividir virtualmente B en una submatriz de tamaño más pequeña (S, S) que se ajusta a la memoria caché.

  para kk en rango (0, K / S + 1):
   para jj en rango (0, M / S + 1):
     para i en rango (0, N):
       para k en el rango (S * kk, min (S * (kk + 1), K)):
         r = A [i] [k]
         para j en el rango (S * jj, min (S * (jj + 1), M)):
           C [i] [j] + = r * B [k] [j]

De esta manera, para cada kk y jj hacemos N * S + S * S accesos a memoria, donde S * S no puede ser mayor que el tamaño de la memoria caché. Con S = 1 volvemos al caso original pero sumamos k, j y luego i. Con S más grande que K y M volvemos al segundo caso con accesos de memoria N * K + K * M, y no podemos hacerlo mejor que eso. De hecho, ¡necesitamos leer ambas matrices para multiplicarlas!

Abordé esto en la respuesta de Victor Eijkhout a ¿Qué algoritmo usa BLAS para la multiplicación de matrices? De todas las consideraciones (por ejemplo, caché, conjuntos de instrucciones populares, Big-O, etc.), ¿cuál resultó ser el principal cuello de botella? Por favor, échale un vistazo y descarga el documento Goto / van de Geijn.