我知道c#中实例化的值类型数组会自动填充该类型的默认值(例如bool为false, int为0,等等)。

是否有一种方法来自动填充一个不是默认的种子值的数组?无论是在创建或之后的内置方法(如Java的Arrays.fill())?假设我想要一个默认为true的布尔数组,而不是false。是否有一个内置的方法来做到这一点,或者你只需要通过一个for循环迭代数组?

 // Example pseudo-code:
 bool[] abValues = new[1000000];
 Array.Populate(abValues, true);

 // Currently how I'm handling this:
 bool[] abValues = new[1000000];
 for (int i = 0; i < 1000000; i++)
 {
     abValues[i] = true;
 }

必须遍历数组并将每个值“重置”为true似乎效率不高。还有其他方法吗?也许通过翻转所有值?

在输入这个问题并思考之后,我猜默认值只是c#在幕后处理这些对象的内存分配的结果,所以我想这可能是不可能的。但我还是想确定一下!


当前回答

下面是另一个被微软抛弃的版本。它的速度是Array的4倍。比Panos Theof的解决方案和Eric J和Petar Petrov的并行解决方案更清晰和更快——对于大型阵列,速度可达两倍。

首先,我想向您介绍函数的祖先,因为这样更容易理解代码。在性能方面,这与Panos Theof的代码相当,对于某些事情来说可能已经足够了:

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);
}

如您所见,这是基于已初始化部分的重复加倍。这是简单而有效的,但它与现代内存架构相冲突。因此诞生了一个版本,它只使用加倍来创建一个缓存友好的种子块,然后在目标区域迭代地爆破:

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);
}

注意:前面的代码需要(count + 1) >> 1作为加倍循环的限制,以确保最终的复制操作有足够的素材来覆盖所有剩余的内容。如果使用计数>> 1来代替奇数,则不会出现这种情况。对于当前版本,这是没有意义的,因为线性复制循环将弥补任何懈怠。

数组单元格的大小必须作为参数传递,因为——令人难以置信的是——泛型不允许使用sizeof,除非它们使用一个约束(非托管),这个约束将来可能可用,也可能不可用。错误的估计不是什么大问题,但如果值是准确的,性能是最好的,原因如下:

低估元素大小可能导致块大小超过L1缓存的一半,因此增加了从L1中删除复制源数据的可能性,并且必须从较慢的缓存级别重新获取。 高估元素大小会导致CPU L1缓存利用率不足,这意味着线性块复制循环的执行次数比最佳利用率时要多。因此,产生的固定循环/调用开销比严格需要的要多。

下面是我的代码与Array的一个基准测试。清除和前面提到的其他三个解决方案。计时用于填充给定大小的整数数组(Int32[])。为了减少缓存异常等引起的变化,每个测试执行两次,背靠背,并在第二次执行时进行计时。

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 

如果这段代码的性能不够,那么一个有希望的途径将是并行线性复制循环(所有线程使用相同的源块),或者我们的老朋友P/Invoke。

Note: clearing and filling of blocks is normally done by runtime routines that branch to highly specialised code using MMX/SSE instructions and whatnot, so in any decent environment one would simply call the respective moral equivalent of std::memset and be assured of professional performance levels. IOW, by rights the library function Array.Clear should leave all our hand-rolled versions in the dust. The fact that it is the other way around shows how far out of whack things really are. Same goes for having to roll one's own Fill<> in the first place, because it is still only in Core and Standard but not in the Framework. .NET has been around for almost twenty years now and we still have to P/Invoke left and right for the most basic stuff or roll our own...

其他回答

Enumerable.Repeat(true, 1000000).ToArray();

创建一个包含1000个真值的新数组:

var items = Enumerable.Repeat<bool>(true, 1000).ToArray();  // Or ToList(), etc.

类似地,你可以生成整数序列:

var items = Enumerable.Range(0, 1000).ToArray();  // 0..999

如果你计划只设置数组中的几个值,但大多数时候想要获得(自定义)默认值,你可以尝试这样做:

public class SparseArray<T>
{
    private Dictionary<int, T> values = new Dictionary<int, T>();

    private T defaultValue;

    public SparseArray(T defaultValue)
    {
        this.defaultValue = defaultValue;
    }

    public T this [int index]
    {
      set { values[index] = value; }
      get { return values.ContainsKey(index) ? values[index] ? defaultValue; }
    }
}

您可能需要实现其他接口以使其有用,例如在数组本身上的接口。

下面是另一个被微软抛弃的版本。它的速度是Array的4倍。比Panos Theof的解决方案和Eric J和Petar Petrov的并行解决方案更清晰和更快——对于大型阵列,速度可达两倍。

首先,我想向您介绍函数的祖先,因为这样更容易理解代码。在性能方面,这与Panos Theof的代码相当,对于某些事情来说可能已经足够了:

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);
}

如您所见,这是基于已初始化部分的重复加倍。这是简单而有效的,但它与现代内存架构相冲突。因此诞生了一个版本,它只使用加倍来创建一个缓存友好的种子块,然后在目标区域迭代地爆破:

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);
}

注意:前面的代码需要(count + 1) >> 1作为加倍循环的限制,以确保最终的复制操作有足够的素材来覆盖所有剩余的内容。如果使用计数>> 1来代替奇数,则不会出现这种情况。对于当前版本,这是没有意义的,因为线性复制循环将弥补任何懈怠。

数组单元格的大小必须作为参数传递,因为——令人难以置信的是——泛型不允许使用sizeof,除非它们使用一个约束(非托管),这个约束将来可能可用,也可能不可用。错误的估计不是什么大问题,但如果值是准确的,性能是最好的,原因如下:

低估元素大小可能导致块大小超过L1缓存的一半,因此增加了从L1中删除复制源数据的可能性,并且必须从较慢的缓存级别重新获取。 高估元素大小会导致CPU L1缓存利用率不足,这意味着线性块复制循环的执行次数比最佳利用率时要多。因此,产生的固定循环/调用开销比严格需要的要多。

下面是我的代码与Array的一个基准测试。清除和前面提到的其他三个解决方案。计时用于填充给定大小的整数数组(Int32[])。为了减少缓存异常等引起的变化,每个测试执行两次,背靠背,并在第二次执行时进行计时。

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 

如果这段代码的性能不够,那么一个有希望的途径将是并行线性复制循环(所有线程使用相同的源块),或者我们的老朋友P/Invoke。

Note: clearing and filling of blocks is normally done by runtime routines that branch to highly specialised code using MMX/SSE instructions and whatnot, so in any decent environment one would simply call the respective moral equivalent of std::memset and be assured of professional performance levels. IOW, by rights the library function Array.Clear should leave all our hand-rolled versions in the dust. The fact that it is the other way around shows how far out of whack things really are. Same goes for having to roll one's own Fill<> in the first place, because it is still only in Core and Standard but not in the Framework. .NET has been around for almost twenty years now and we still have to P/Invoke left and right for the most basic stuff or roll our own...

并行实现怎么样

public static void InitializeArray<T>(T[] array, T value)
{
    var cores = Environment.ProcessorCount;

    ArraySegment<T>[] segments = new ArraySegment<T>[cores];

    var step = array.Length / cores;
    for (int i = 0; i < cores; i++)
    {
        segments[i] = new ArraySegment<T>(array, i * step, step);
    }
    var remaining = array.Length % cores;
    if (remaining != 0)
    {
        var lastIndex = segments.Length - 1;
        segments[lastIndex] = new ArraySegment<T>(array, lastIndex * step, array.Length - (lastIndex * step));
    }

    var initializers = new Task[cores];
    for (int i = 0; i < cores; i++)
    {
        var index = i;
        var t = new Task(() =>
        {
            var s = segments[index];
            for (int j = 0; j < s.Count; j++)
            {
                array[j + s.Offset] = value;
            }
        });
        initializers[i] = t;
        t.Start();
    }

    Task.WaitAll(initializers);
}

当只初始化一个数组时,这段代码的力量是看不出来的,但我认为你绝对应该忘记“纯”for。