Aqui está outra versão para nós, usuários do Framework abandonados pela Microsoft. É 4 vezes mais rápido que Array.Clear
e mais rápido do que a solução da Panos Theof e Eric J de e uma paralela do Petar Petrov - até duas vezes mais rápido para grandes matrizes.
Primeiro, quero apresentar o ancestral da função, porque isso facilita a compreensão do código. Em termos de desempenho, isso é parecido com o código de Panos Theof, e para algumas coisas que já podem 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 você pode ver, isso se baseia na duplicação repetida da parte já inicializada. Isso é simples e eficiente, mas está em conflito com as arquiteturas de memória modernas. Portanto, nasceu uma versão que usa a duplicação apenas para criar um bloco de propagação compatível com cache, que é distribuído iterativamente pela área de destino:
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: o código anterior necessário (count + 1) >> 1
como limite para o ciclo de duplicação para garantir que a operação de cópia final tenha forragem suficiente para cobrir tudo o que resta. Esse não seria o caso de contagens ímpares, se count >> 1
fosse utilizado em seu lugar. Para a versão atual, isso não tem significado, uma vez que o loop de cópia linear receberá qualquer folga.
O tamanho de uma célula de matriz deve ser passado como parâmetro porque - a mente desconcerta - os genéricos não podem usar, a sizeof
menos que usem uma restrição ( unmanaged
) que pode ou não estar disponível no futuro. Estimativas erradas não são um grande problema, mas o desempenho é melhor se o valor for preciso, pelos seguintes motivos:
Subestimar o tamanho do elemento pode levar a tamanhos de bloco maiores que a metade do cache L1, aumentando a probabilidade de os dados da fonte de cópia serem despejados do L1 e precisando ser buscados novamente a partir de níveis mais lentos do cache.
Superestimar o tamanho do elemento resulta na subutilização do cache L1 da CPU, o que significa que o loop de cópia de bloco linear é executado com mais frequência do que seria com a utilização ideal. Assim, ocorre mais sobrecarga de loop / chamada fixa do que o estritamente necessário.
Aqui está uma referência comparando meu código Array.Clear
e as outras três soluções mencionadas anteriormente. Os horários são para o preenchimento de matrizes inteiras ( Int32[]
) dos tamanhos fornecidos. Para reduzir a variação causada por caprichos no cache, etc., cada teste foi executado duas vezes, consecutivamente, e os tempos foram tomados para a segunda execução.
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
Se o desempenho desse código não for suficiente, um caminho promissor seria paralelo ao loop de cópia linear (com todos os threads usando o mesmo bloco de origem) ou ao nosso bom e velho amigo P / Invoke.
Nota: a limpeza e o preenchimento de blocos normalmente são feitos por rotinas de tempo de execução que ramificam para código altamente especializado usando instruções MMX / SSE e outras coisas, portanto, em qualquer ambiente decente, basta chamar o respectivo equivalente moral std::memset
e garantir níveis de desempenho profissional. IOW, por direitos, a função de biblioteca Array.Clear
deve deixar todas as nossas versões enroladas à mão na poeira. O fato de ser o contrário mostra o quão longe as coisas estão realmente. O mesmo vale para ter que rolar a si próprio Fill<>
em primeiro lugar, porque ainda está apenas no Core e no Standard, mas não no Framework. O .NET já existe há quase vinte anos e ainda temos que P / Invocar para a esquerda e para a direita para obter as coisas mais básicas ou ...