如何用一个值填充/实例化C#数组?

285
我知道在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#在幕后处理这些对象的内存分配时的结果,所以我想这可能是不可能的。但我仍然想确切知道!


我通常将变量名从is_found更改为is_still_hiding。非常感谢提供的答案,我在测试用例中需要对int数组进行类似处理。(好问题) - ctrl-alt-delor
创建一个新的结构体,以实际使用您想要的默认值,也许? - arkon
26个回答

9

如果你使用的是 .NET Core、.NET Standard >= 2.1 或者依赖 System.Memory 包,那么你也可以使用 Span<T>.Fill() 方法:

var valueToFill = 165;
var data = new int[100];

data.AsSpan().Fill(valueToFill);

// print array content
for (int i = 0; i < data.Length; i++)
{
    Console.WriteLine(data[i]);
}

https://dotnetfiddle.net/UsJ9bu


7

或者…你可以使用反向逻辑。让false表示true,反之亦然。

代码示例

// bool[] isVisible = Enumerable.Repeat(true, 1000000).ToArray();
bool[] isHidden = new bool[1000000]; // Crazy-fast initialization!

// if (isVisible.All(v => v))
if (isHidden.All(v => !v))
{
    // Do stuff!
}

有趣的解决方案,尽管如果使用整数会更难,因为你会失去0。 - MrFox
1
如果你将变量名的逻辑反转,这实际上是一个可行的选项:将 bool[] isVisible 改为 bool[] isHidden - Markus Hütter
2
人们似乎对这种技巧有些反应过度,好像这是某种有趣的黑客行为。实际上,这是一种常见的优化技术。如果你很幸运,编译器会自动为你完成这个过程。 - l33t
不是你要表达的重点,但这里有另一种倒置逻辑:你可以使用!isHidden.Contains(true)代替isHidden.All(v => !v)中的lambda。 - Jeppe Stig Nielsen

7

关于并行实现怎么样?

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循环。


1
这会存在虚假共享的问题,即不同线程竞争CPU缓存行,从而降低性能,与单线程实现相比。是否发生这种情况取决于每个线程内存块的大小和CPU架构。 - Eric J.

7

这里给出的大多数答案都归结为一个循环,逐个初始化数组,这种方法无法利用CPU指令来一次操作一块内存。

.Net Standard 2.1(此时正在预览中)提供了Array.Fill(),它适用于在运行时库中高性能实现(尽管目前为止,.NET Core 似乎没有利用这个可能性)。

对于早期平台的用户,在数组大小显著时,以下扩展方法比单纯的循环快得多。我创建它时是为了解决在线代码挑战的方案超出了分配的时间预算约20%。它将运行时间缩短了大约70%。在这种情况下,数组填充是在另一个循环内执行的。BLOCK_SIZE是通过直觉而不是实验设置的。一些优化是可能的(例如,复制所有已设置为所需值的字节,而不是固定大小的块)。

internal const int BLOCK_SIZE = 256;
public static void Fill<T>(this T[] array, T value)
{
    if (array.Length < 2 * BLOCK_SIZE)
    {
        for (int i = 0; i < array.Length; i++) array[i] = value;
    }
    else
    {
        int fullBlocks = array.Length / BLOCK_SIZE;
        // Initialize first block
        for (int j = 0; j < BLOCK_SIZE; j++) array[j] = value;
        // Copy successive full blocks
        for (int blk = 1; blk < fullBlocks; blk++)
        {
            Array.Copy(array, 0, array, blk * BLOCK_SIZE, BLOCK_SIZE);
        }

        for (int rem = fullBlocks * BLOCK_SIZE; rem < array.Length; rem++)
        {
            array[rem] = value;
        }
    }
}

blkBLOCK_SIZE 递增可能比乘法更值得。当然,正确的答案是让 .Net Core 优化 Array.Fill<T> - NetMage

7

仅供参考:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.997 (1909/November2018Update/19H2)
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.302
  [Host]        : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT
  .NET Core 3.1 : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT

Job=.NET Core 3.1  Runtime=.NET Core 3.1

|           Method |     Mean |     Error |    StdDev |
|----------------- |---------:|----------:|----------:|
| EnumerableRepeat | 2.311 us | 0.0228 us | 0.0213 us |
|  NewArrayForEach | 2.007 us | 0.0392 us | 0.0348 us |
|        ArrayFill | 2.426 us | 0.0103 us | 0.0092 us |

    [SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.NetCoreApp31)]
    public class InitializeArrayBenchmark {
        const int ArrayLength = 1600;

        [Benchmark]
        public double[] EnumerableRepeat() {
            return Enumerable.Repeat(double.PositiveInfinity, ArrayLength).ToArray();
        }

        [Benchmark]
        public double[] NewArrayForEach() {
            var array = new double[ArrayLength];

            for (int i = 0; i < array.Length; i++) {
                array[i] = double.PositiveInfinity;
            }

            return array;
        }

        [Benchmark]
        public double[] ArrayFill() {
            var array = new double[ArrayLength];
            Array.Fill(array, double.PositiveInfinity);

            return array;
        }
    }

你的基准测试有缺陷。在这样一个小而孤立的环境中,差异仅仅是纳秒级别的时候,很难获得可靠的结果。Array.Fill 在性能上更为优越,因为 Fill 只需要访问类成员地址一次。 (并且,像库函数一样,Fill 也可以从任何未来的优化中受益。) - arkon

4
这也可以起作用...但可能是不必要的。
 bool[] abValues = new bool[1000];
 abValues = abValues.Select( n => n = true ).ToArray<bool>();

4

除非那个值是元素类型的默认值,否则没有办法将数组中的所有元素设置为单个操作。

例如,如果它是整数数组,则可以使用单个操作将它们全部设置为零,如下所示:Array.Clear(...)


4

这是另一个版本,供那些被微软抛弃的框架用户使用。它比 Array.Clear 快4倍,比Panos Theof's solutionEric J's 以及Petar Petrov's parallel one更快 - 对于大型数组,速度可高达两倍。

首先,我想给大家介绍一下这个函数的先驱,因为这样更容易理解代码。从性能上看,这与 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 作为加倍循环的限制条件,以确保最终复制操作有足够的材料来覆盖所有剩余的内容。如果使用 count >> 1,则对于奇数计数,情况将不同。对于当前版本而言,这没有任何意义,因为线性复制循环将捡起任何闲置。

必须传递数组单元格的大小作为参数,因为 - 令人费解的是 - 泛型不能使用 sizeof,除非它们使用了一个约束条件 (unmanaged),这个约束条件可能会在将来可用,也可能不可用。错误的估计并不是什么大问题,但如果该值准确,性能最佳,原因如下:

  • 低估元素大小可能导致块大小大于 L1 缓存的一半,从而增加从 L1 驱逐复制源数据并必须从较慢的高速缓存级别重新获取的可能性。

  • 高估元素大小会导致 CPU 的 L1 缓存被低效利用,这意味着线性块复制循环比最优情况下执行得更频繁。因此,比必需的更多的固定循环/调用开销都会产生。

以下是基准测试,将我的代码与Array.Clear 和先前提到的其他三种解决方案进行比较。计时是针对给定大小的整数数组 (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。
注意:块的清除和填充通常由运行时例程完成,这些例程分支到高度专门的代码,使用 MMX/SSE 指令等等,因此在任何合适的环境中,只需调用相应的道德等价物 std::memset,就可以保证专业的性能水平。换句话说,按照权利,库函数 Array.Clear 应该让我们手动编写的版本望尘莫及。事实上相反,这表明了事情的严重不协调。同样适用于首先必须自己编写 Fill<> 的情况,因为它仍然只存在于 Core 和 Standard 中,而不是 Framework 中。.NET 已经存在了将近二十年,我们仍然不得不不断地进行 P/Invoke 来处理最基本的问题或自己动手...

2
就此而言,C语言的memset函数不适用于宽度超过一个字节的模式。如果你使用Buffer.BlockCopy而非Array.Copy,C#将为你提供快速的SIMD复制...但是对于任何聚合类型,它都会抛出异常,因为BlockCopy仅允许原始类型。如果使用BlockCopy,请注意偏移量和长度参数不是与Array.Copy相同的单位。 - Ben Voigt
L1缓存大小不应该是65536吗?(它被二分之一:int block_size = L1_CACHE_SIZE / element_size / 2) - smirkingman
@smirkingman:我将其除以2是因为新写入的值也会出现在L1中。请注意Ben Voigt关于使用Buffer.BlockCopy的评论;这是一个重要的优化。 - DarthGizka

3
如果您计划仅设置数组中的一些值,但大多数时间都希望获得(自定义)默认值,则可以尝试以下方法:
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; }
    }
}

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

3

我有点惊讶,没有人做出非常简单但超快的SIMD版本:

  public static void PopulateSimd<T>(T[] array, T value) where T : struct
  {
     var vector = new Vector<T>(value);
     var i = 0;
     var s = Vector<T>.Count;
     var l = array.Length & ~(s-1);
     for (; i < l; i += s) vector.CopyTo(array, i);
     for (; i < array.Length; i++) array[i] = value;
  }

基准测试:(数字是针对Framework 4.8,但Core3.1在统计上相同)

|     方法 |       N |        平均值 |          误差 |      标准偏差 |  比率 | 比率标准差 |
|--------- |-------- |------------:|------------:|-------------:|------:|----------:|
| DarthGizka |      10 |      25.975 纳秒 |      1.2430 纳秒 |     0.1924 纳秒 |  1.00 |    0.00 |
|       Simd |      10 |       3.438 纳秒 |      0.4427 纳秒 |     0.0685 纳秒 |  0.13 |    0.00 |
|            |         |                |                |               |       |         |
| DarthGizka |     100 |      81.155 纳秒 |      3.8287 纳秒 |     0.2099 纳秒 |  1.00 |    0.00 |
|       Simd |     100 |      12.178 纳秒 |      0.4547 纳秒 |     0.0704 纳秒 |  0.15 |    0.00 |
|            |         |                |                |               |       |         |
| DarthGizka |    1000 |     201.138 纳秒 |      8.9769 纳秒 |     1.3892 纳秒 |  1.00 |    0.00 |
|       Simd |    1000 |     100.397 纳秒 |      4.0965 纳秒 |     0.6339 纳秒 |  0.50 |    0.00 |
|            |         |                |                |               |       |         |
| DarthGizka |   10000 |   1,292.660 纳秒 |     38.4965 纳秒 |     5.9574 纳秒 |  1.00 |    0.00 |
|       Simd |   10000 |   1,272.819 纳秒 |     68.5148 纳秒 |    10.6027 纳秒 |  0.98 |    0.01 |
|            |         |                |                |               |       |         |
| DarthGizka |  100000 |  16,156.106 纳秒 |    366.1133 纳秒 |    56.6564 纳秒 |  1.00 |    0.00 |
|       Simd |  100000 |  17,627.879 纳秒 |  1,589.7423 纳秒 |   246.0144 纳秒 |  1.09 |    0.02 |
|            |         |                |                |               |       |         |
| DarthGizka | 1000000 | 176,625.870 纳秒 | 32,235.9957 纳秒 | 1,766.9637 纳秒 |  1.00 |    0.00 |
|       Simd | 1000000 | 186,812.920 纳秒 | 18,069.1517 纳秒 | 2,796.2212 纳秒 |  1.07 |    0.01 |

如图所示,在少于10000个元素的情况下速度更快,而在此之上则稍微慢了一些。


什么命名空间?这对于struct数组有效还是仅适用于基元类型? - Ben Voigt
https://learn.microsoft.com/en-us/dotnet/api/system.numerics.vector-1?view=net-5.0 - Cine

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接