Aquí hay otra versión para nosotros, usuarios de Framework abandonados por Microsoft. Es 4 veces más rápido que Array.Clear
y más rápido que la solución de Panos Theof y Eric J y una paralela de Petar Petrov - hasta dos veces más rápido para grandes matrices.
Primero quiero presentarles el antepasado de la función, porque eso facilita la comprensión del código. En cuanto al rendimiento, esto está bastante a la par con el código de Panos Theof, y para algunas cosas que ya pueden ser suficientes:
public static void Fill<T> (T[] array, int count, T value, int threshold = 32)
{
if (threshold <= 0)
throw new ArgumentException("threshold");
int current_size = 0, keep_looping_up_to = Math.Min(count, threshold);
while (current_size < keep_looping_up_to)
array[current_size++] = value;
for (int at_least_half = (count + 1) >> 1; current_size < at_least_half; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);
Array.Copy(array, 0, array, current_size, count - current_size);
}
Como puede ver, esto se basa en la duplicación repetida de la parte ya inicializada. Esto es simple y eficiente, pero está en conflicto con las arquitecturas de memoria modernas. Por lo tanto, nació una versión que usa la duplicación solo para crear un bloque de semillas amigable con la caché, que luego se lanza de forma iterativa sobre el área objetivo:
const int ARRAY_COPY_THRESHOLD = 32; // 16 ... 64 work equally well for all tested constellations
const int L1_CACHE_SIZE = 1 << 15;
public static void Fill<T> (T[] array, int count, T value, int element_size)
{
int current_size = 0, keep_looping_up_to = Math.Min(count, ARRAY_COPY_THRESHOLD);
while (current_size < keep_looping_up_to)
array[current_size++] = value;
int block_size = L1_CACHE_SIZE / element_size / 2;
int keep_doubling_up_to = Math.Min(block_size, count >> 1);
for ( ; current_size < keep_doubling_up_to; current_size <<= 1)
Array.Copy(array, 0, array, current_size, current_size);
for (int enough = count - block_size; current_size < enough; current_size += block_size)
Array.Copy(array, 0, array, current_size, block_size);
Array.Copy(array, 0, array, current_size, count - current_size);
}
Nota: el código anterior era necesario (count + 1) >> 1
como límite para el ciclo de duplicación para garantizar que la operación de copia final tenga suficiente forraje para cubrir todo lo que queda. Este no sería el caso para los recuentos impares si count >> 1
se usaran en su lugar. Para la versión actual esto no tiene importancia ya que el bucle de copia lineal recogerá cualquier holgura.
El tamaño de una celda de matriz debe pasarse como un parámetro porque, la mente alucina, no se permite el uso de genéricos a sizeof
menos que usen una restricción ( unmanaged
) que puede estar disponible o no en el futuro. Las estimaciones incorrectas no son un gran problema, pero el rendimiento es mejor si el valor es exacto, por las siguientes razones:
Subestimar el tamaño del elemento puede dar lugar a tamaños de bloque superiores a la mitad de la memoria caché L1, lo que aumenta la probabilidad de que los datos de origen de la copia sean expulsados de L1 y tengan que volverse a buscar desde niveles de memoria caché más lentos.
Sobreestimar el tamaño del elemento da como resultado una infrautilización de la memoria caché L1 de la CPU, lo que significa que el bucle de copia en bloque lineal se ejecuta con más frecuencia de lo que sería con una utilización óptima. Por lo tanto, se incurre en una mayor sobrecarga de bucle / llamada fija de lo estrictamente necesario.
Aquí hay un punto de referencia que enfrenta mi código Array.Clear
y las otras tres soluciones mencionadas anteriormente. Los tiempos son para llenar matrices enteras ( Int32[]
) de los tamaños dados. Para reducir la variación causada por los caprichos de la memoria caché, etc., cada prueba se ejecutó dos veces, una tras otra, y los tiempos se tomaron para la segunda ejecución.
array size Array.Clear Eric J. Panos Theof Petar Petrov Darth Gizka
-------------------------------------------------------------------------------
1000: 0,7 µs 0,2 µs 0,2 µs 6,8 µs 0,2 µs
10000: 8,0 µs 1,4 µs 1,2 µs 7,8 µs 0,9 µs
100000: 72,4 µs 12,4 µs 8,2 µs 33,6 µs 7,5 µs
1000000: 652,9 µs 135,8 µs 101,6 µs 197,7 µs 71,6 µs
10000000: 7182,6 µs 4174,9 µs 5193,3 µs 3691,5 µs 1658,1 µs
100000000: 67142,3 µs 44853,3 µs 51372,5 µs 35195,5 µs 16585,1 µs
Si el rendimiento de este código no es suficiente, una vía prometedora sería paralelizar el bucle de copia lineal (con todos los hilos utilizando el mismo bloque de origen), o nuestro buen viejo amigo P / Invoke.
Nota: la limpieza y el llenado de bloques normalmente se realiza mediante rutinas de tiempo de ejecución que se ramifican a código altamente especializado utilizando instrucciones MMX / SSE y demás, por lo que en cualquier entorno decente uno simplemente llamaría el equivalente moral respectivo std::memset
y estaría seguro de los niveles de rendimiento profesional. IOW, por derecho, la función de biblioteca Array.Clear
debería dejar todas nuestras versiones enrolladas a mano en el polvo. El hecho de que sea al revés muestra cuán fuera de control están realmente las cosas. Lo mismo ocurre con tener que rodar la propia Fill<>
en primer lugar, porque todavía está solo en Core y Standard, pero no en el Framework. .NET ha existido durante casi veinte años y todavía tenemos que P / Invocar de izquierda a derecha para las cosas más básicas o rodar el nuestro ...