我知道 C# 中的值类型的实例化数组会自动填充 default value of the type(例如,布尔为 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# 如何在幕后处理这些对象的内存分配的结果,所以我想这可能是不可能的。但我还是想确定一下!
Enumerable.Repeat(true, 1000000).ToArray();
不知道框架方法,但您可以编写一个快速助手来为您完成。
public static void Populate<T>(this T[] arr, T value ) {
for ( int i = 0; i < arr.Length;i++ ) {
arr[i] = value;
}
}
int[] arr = new int[16].Populate(-1);
void
更改为 T[]
然后您可以执行 var a = new int[100].Polupate(1)
创建一个包含一千个 true
值的新数组:
var items = Enumerable.Repeat<bool>(true, 1000).ToArray(); // Or ToList(), etc.
同样,您可以生成整数序列:
var items = Enumerable.Range(0, 1000).ToArray(); // 0..999
您可以在 .NET Core 2.0+ 和 .NET Standard 2.1+ 中使用 Array.Fill
。
Array.Fill(myArray, myDefaultValue);
对于大型数组或可变大小的数组,您可能应该使用:
Enumerable.Repeat(true, 1000000).ToArray();
对于小型数组,您可以使用 C# 3 中的集合初始化语法:
bool[] vals = new bool[]{ false, false, false, false, false, false, false };
集合初始化语法的好处是您不必在每个槽中使用相同的值,您可以使用表达式或函数来初始化槽。另外,我认为您可以避免将数组槽初始化为默认值的成本。因此,例如:
bool[] vals = new bool[]{ false, true, false, !(a ||b) && c, SomeBoolMethod() };
float[] AlzCalDefault = new float[] {(float) 0.5, 18, 500, 1, 0};
bool[] vals = { false, true, false, !(a || b) && c, SomeBoolMethod() };
如果您的数组太大,您应该使用 BitArray。它为每个 bool 使用 1 位而不是一个字节(如在 bool 数组中),您也可以使用位运算符将所有位设置为 true。或者只是初始化为true。如果你只需要做一次,它只会花费更多。
System.Collections.BitArray falses = new System.Collections.BitArray(100000, false);
System.Collections.BitArray trues = new System.Collections.BitArray(100000, true);
// Now both contain only true values.
falses.And(trues);
不幸的是,我认为没有直接的方法,但是我认为您可以为数组类编写一个扩展方法来做到这一点
class Program
{
static void Main(string[] args)
{
int[] arr = new int[1000];
arr.Init(10);
Array.ForEach(arr, Console.WriteLine);
}
}
public static class ArrayExtensions
{
public static void Init<T>(this T[] array, T defaultVaue)
{
if (array == null)
return;
for (int i = 0; i < array.Length; i++)
{
array[i] = defaultVaue;
}
}
}
好吧,经过更多的谷歌搜索和阅读,我发现了这个:
bool[] bPrimes = new bool[1000000];
bPrimes = Array.ConvertAll<bool, bool>(bPrimes, b=> b=true);
这肯定更接近我正在寻找的东西。但我不确定这是否比在 for 循环中遍历原始数组并仅更改值更好。事实上,经过快速测试,它看起来慢了大约 5 倍。所以这不是一个好的解决方案!
下面的代码结合了小副本的简单迭代和大副本的 Array.Copy
public static void Populate<T>( T[] array, int startIndex, int count, T value ) {
if ( array == null ) {
throw new ArgumentNullException( "array" );
}
if ( (uint)startIndex >= array.Length ) {
throw new ArgumentOutOfRangeException( "startIndex", "" );
}
if ( count < 0 || ( (uint)( startIndex + count ) > array.Length ) ) {
throw new ArgumentOutOfRangeException( "count", "" );
}
const int Gap = 16;
int i = startIndex;
if ( count <= Gap * 2 ) {
while ( count > 0 ) {
array[ i ] = value;
count--;
i++;
}
return;
}
int aval = Gap;
count -= Gap;
do {
array[ i ] = value;
i++;
--aval;
} while ( aval > 0 );
aval = Gap;
while ( true ) {
Array.Copy( array, startIndex, array, i, aval );
i += aval;
count -= aval;
aval *= 2;
if ( count <= aval ) {
Array.Copy( array, startIndex, array, i, count );
break;
}
}
}
使用 int[] 数组的不同数组长度的基准是:
2 Iterate: 1981 Populate: 2845
4 Iterate: 2678 Populate: 3915
8 Iterate: 4026 Populate: 6592
16 Iterate: 6825 Populate: 10269
32 Iterate: 16766 Populate: 18786
64 Iterate: 27120 Populate: 35187
128 Iterate: 49769 Populate: 53133
256 Iterate: 100099 Populate: 71709
512 Iterate: 184722 Populate: 107933
1024 Iterate: 363727 Populate: 126389
2048 Iterate: 710963 Populate: 220152
4096 Iterate: 1419732 Populate: 291860
8192 Iterate: 2854372 Populate: 685834
16384 Iterate: 5703108 Populate: 1444185
32768 Iterate: 11396999 Populate: 3210109
第一列是数组大小,然后是使用简单迭代(@JaredPared 实现)进行复制的时间。这种方法的时间是在那之后。这些是使用四个整数结构的数组的基准
2 Iterate: 2473 Populate: 4589
4 Iterate: 3966 Populate: 6081
8 Iterate: 7326 Populate: 9050
16 Iterate: 14606 Populate: 16114
32 Iterate: 29170 Populate: 31473
64 Iterate: 57117 Populate: 52079
128 Iterate: 112927 Populate: 75503
256 Iterate: 226767 Populate: 133276
512 Iterate: 447424 Populate: 165912
1024 Iterate: 890158 Populate: 367087
2048 Iterate: 1786918 Populate: 492909
4096 Iterate: 3570919 Populate: 1623861
8192 Iterate: 7136554 Populate: 2857678
16384 Iterate: 14258354 Populate: 6437759
32768 Iterate: 28351852 Populate: 12843259
并行实现怎么样
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);
}
仅初始化数组时,看不到此代码的功能,但我认为您绝对应该忘记“纯”。
或者......你可以简单地使用反转逻辑。让 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!
}
bool[] isVisible
使其成为 bool[] isHidden
这里提供的许多答案都归结为一个循环,该循环一次初始化数组一个元素,它没有利用设计用于一次操作一块内存的 CPU 指令。
.Net Standard 2.1(在撰写本文时为预览版)提供了 Array.Fill(),它有助于在运行时库中实现高性能(尽管到目前为止,.NET Core doesn't seem to 利用了这种可能性)。
对于早期平台上的那些,当数组大小很大时,以下扩展方法比普通循环的性能要好很多。当我的在线代码挑战解决方案超出分配的时间预算约 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;
}
}
}
blk
增加 BLOCK_SIZE
而不是相乘可能是值得的。当然,正确的答案是 .Net Core 优化 Array.Fill<T>
。
如果您使用的是 .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
.NET Core 2.0 及更高版本支持 Array.Fill()
方法。
这是一个示例代码。
var arr = new int[10];
int defaultValue = 2;
Array.Fill(arr,defaultValue);
它还具有用于填充索引范围的重载方法。更多详细信息,请参见here。
只是一个基准:
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
将从未来的任何优化中受益。)
这也有效......但可能是不必要的
bool[] abValues = new bool[1000];
abValues = abValues.Select( n => n = true ).ToArray<bool>();
无法将数组中的所有元素设置为单个操作,除非该值是元素类型的默认值。
例如,如果它是一个整数数组,您可以通过一次操作将它们全部设置为零,如下所示:Array.Clear(...)
这是微软放弃的我们框架用户的另一个版本。它的速度是 Array.Clear
的 4 倍,比 Panos Theof's solution 和 Eric 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
并确保专业性能水平。 IOW,按照权利,库函数 Array.Clear
应该让我们所有的手卷版本都尘埃落定。事实恰恰相反,这表明事情真的很不正常。首先必须推出自己的 Fill<>
也是如此,因为它仍然只在核心和标准中,而不是在框架中。 .NET 已经存在将近 20 年了,我们仍然需要左右 P/Invoke 来获取最基本的东西,或者推出我们自己的...
memset
不适用于宽于一个字节的模式。如果您使用 Buffer.BlockCopy
而不是 Array.Copy
,C# 将为您提供快速 SIMD 副本......但是它会为任何聚合类型抛出异常,BlockCopy 仅允许用于原始类型。如果使用 BlockCopy,还要注意偏移量和长度参数与 Array.Copy 的单位不同。
如果您打算只设置数组中的几个值,但想在大多数情况下获取(自定义)默认值,您可以尝试以下操作:
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 本身上的那些。
我意识到我参加聚会迟到了,但这里有个主意。编写一个包装器,该包装器具有与包装值之间的转换运算符,以便它可以用作包装类型的替身。这实际上是受到@l33t 听起来很愚蠢的回答的启发。
首先(来自 C++)我意识到在 C# 中,构造数组元素时不会调用默认 ctor。相反——即使存在用户定义的默认构造函数! -- 所有数组元素都是零初始化的。这确实让我感到惊讶。
因此,仅提供具有所需值的默认 ctor 的包装类将适用于 C++ 中的数组,但不适用于 C#。一种解决方法是让包装器类型在转换时将 0 映射到所需的种子值。这样,出于所有实际目的,零初始化值似乎是用种子初始化的:
public struct MyBool
{
private bool _invertedValue;
public MyBool(bool b)
{
_invertedValue = !b;
}
public static implicit operator MyBool(bool b)
{
return new MyBool(b);
}
public static implicit operator bool(MyBool mb)
{
return !mb._invertedValue;
}
}
static void Main(string[] args)
{
MyBool mb = false; // should expose false.
Console.Out.WriteLine("false init gives false: "
+ !mb);
MyBool[] fakeBoolArray = new MyBool[100];
Console.Out.WriteLine("Default array elems are true: "
+ fakeBoolArray.All(b => b) );
fakeBoolArray[21] = false;
Console.Out.WriteLine("Assigning false worked: "
+ !fakeBoolArray[21]);
fakeBoolArray[21] = true;
// Should define ToString() on a MyBool,
// hence the !! to force bool
Console.Out.WriteLine("Assigning true again worked: "
+ !!fakeBoolArray[21]);
}
此模式适用于所有值类型。例如,如果需要使用 4 进行初始化等,可以将整数映射为 0 到 4 等。
我很想像在 C++ 中那样制作它的模板,提供种子值作为模板参数,但我知道这在 C# 中是不可能的。还是我错过了什么? (当然在 C++ 中映射是完全没有必要的,因为可以提供一个默认的 ctor 来调用数组元素。)
FWIW,这是一个 C++ 等价物: https://ideone.com/wG8yEh 。
如果您可以反转您的逻辑,则可以使用 Array.Clear()
方法将布尔数组设置为 false。
int upperLimit = 21;
double optimizeMe = Math.Sqrt(upperLimit);
bool[] seiveContainer = new bool[upperLimit];
Array.Clear(seiveContainer, 0, upperLimit);
我有点惊讶没有人制作非常简单但超快的 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 在统计上是相同的)
| Method | N | Mean | Error | StdDev | Ratio | RatioSD | |----------- |-------- |---------------:|---------------:|--------------:|------:|--------:| | DarthGizka | 10 | 25.975 ns | 1.2430 ns | 0.1924 ns | 1.00 | 0.00 | | Simd | 10 | 3.438 ns | 0.4427 ns | 0.0685 ns | 0.13 | 0.00 | | | | | | | | | | DarthGizka | 100 | 81.155 ns | 3.8287 ns | 0.2099 ns | 1.00 | 0.00 | | Simd | 100 | 12.178 ns | 0.4547 ns | 0.0704 ns | 0.15 | 0.00 | | | | | | | | | | DarthGizka | 1000 | 201.138 ns | 8.9769 ns | 1.3892 ns | 1.00 | 0.00 | | Simd | 1000 | 100.397 ns | 4.0965 ns | 0.6339 ns | 0.50 | 0.00 | | | | | | | | | | DarthGizka | 10000 | 1,292.660 ns | 38.4965 ns | 5.9574 ns | 1.00 | 0.00 | | Simd | 10000 | 1,272.819 ns | 68.5148 ns | 10.6027 ns | 0.98 | 0.01 | | | | | | | | | | DarthGizka | 100000 | 16,156.106 ns | 366.1133 ns | 56.6564 ns | 1.00 | 0.00 | | Simd | 100000 | 17,627.879 ns | 1,589.7423 ns | 246.0144 ns | 1.09 | 0.02 | | | | | | | | | | DarthGizka | 1000000 | 176,625.870 ns | 32,235.9957 ns | 1,766.9637 ns | 1.00 | 0.00 | | Simd | 1000000 | 186,812.920 ns | 18,069.1517 ns | 2,796.2212 ns | 1.07 | 0.01 |
可以看出,它在 <10000 个元素时要快得多,而在此之后只会稍微慢一些。
struct
数组还是仅适用于原语?
关于这个(重复?)问题还有更多答案:What is the equivalent of memset in C#?
有人对替代方案进行了基准测试(他们包括不安全的版本,但他们没有尝试 memset
):http://techmikael.blogspot.co.uk/2009/12/filling-array-with-default-value.html
这是 System.Collections.BitArray
的另一种方法,它具有这样的构造函数。
bool[] result = new BitArray(1000000, true).Cast<bool>().ToArray();
或者
bool[] result = new bool[1000000];
new BitArray(1000000, true).CopyTo(result, 0);
在创建数组的地方创建一个私有类,并为其设置一个 getter 和 setter。除非您需要数组中的每个位置都是唯一的,例如随机,否则使用 int?作为一个数组,然后在获取位置是否相等时填充该位置并返回新的随机值。
IsVisibleHandler
{
private bool[] b = new bool[10000];
public bool GetIsVisible(int x)
{
return !b[x]
}
public void SetIsVisibleTrueAt(int x)
{
b[x] = false //!true
}
}
或使用
public void SetIsVisibleAt(int x, bool isTrue)
{
b[x] = !isTrue;
}
作为二传手。
Boolean[] data = new Boolean[25];
new Action<Boolean[]>((p) => { BitArray seed = new BitArray(p.Length, true); seed.CopyTo(p, 0); }).Invoke(data);
不定期副业成功案例分享
Enumerable.ToArray
不知道可枚举序列的大小,因此它必须猜测数组大小。这意味着每次超出ToArray
的缓冲区时,您都会获得数组分配,并在最后再分配一个用于修剪的分配。可枚举对象还涉及开销。