99

我需要byte[]用一个非零值填充 a 。如何在 C# 中执行此操作而不遍历byte数组中的每个?

更新:评论似乎将其分为两个问题-

  1. 是否有框架方法来填充可能类似于的 byte[]memset
  2. 当我们处理一个非常大的数组时,最有效的方法是什么?

正如 Eric 和其他人所指出的,我完全同意使用简单的循环就可以了。问题的重点是看看我是否可以学到一些关于 C# 的新知识:) 我认为 Juliet 的并行操作方法应该比简单的循环更快。

基准测试: 感谢 Mikael Svenson:http ://techmikael.blogspot.com/2009/12/filling-array-with-default-value.html

事实证明,for除非您想使用不安全的代码,否则简单循环是可行的方法。

很抱歉在我的原始帖子中没有更清楚。Eric 和 Mark 的评论都是正确的;肯定需要有更集中的问题。感谢大家的建议和回复。

4

16 回答 16

64

你可以使用Enumerable.Repeat

byte[] a = Enumerable.Repeat((byte)10, 100).ToArray();

第一个参数是要重复的元素,第二个参数是重复的次数。

这对于小型数组是可以的,但如果您正在处理非常大的数组并且性能是一个问题,则应该使用循环方法。

于 2009-12-13T20:02:52.153 回答
53

实际上,鲜为人知的 IL 操作称为Initblk英文版)正是这样做的。因此,让我们将其用作不需要“不安全”的方法。这是助手类:

public static class Util
{
    static Util()
    {
        var dynamicMethod = new DynamicMethod("Memset", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard,
            null, new [] { typeof(IntPtr), typeof(byte), typeof(int) }, typeof(Util), true);

        var generator = dynamicMethod.GetILGenerator();
        generator.Emit(OpCodes.Ldarg_0);
        generator.Emit(OpCodes.Ldarg_1);
        generator.Emit(OpCodes.Ldarg_2);
        generator.Emit(OpCodes.Initblk);
        generator.Emit(OpCodes.Ret);

        MemsetDelegate = (Action<IntPtr, byte, int>)dynamicMethod.CreateDelegate(typeof(Action<IntPtr, byte, int>));
    }

    public static void Memset(byte[] array, byte what, int length)
    {
        var gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);
        MemsetDelegate(gcHandle.AddrOfPinnedObject(), what, length);
        gcHandle.Free();
    }

    public static void ForMemset(byte[] array, byte what, int length)
    {
        for(var i = 0; i < length; i++)
        {
            array[i] = what;
        }
    }

    private static Action<IntPtr, byte, int> MemsetDelegate;

}

表现如何?这是我对 Windows/.NET 和 Linux/Mono(不同的 PC)的结果。

Mono/for:     00:00:01.1356610
Mono/initblk: 00:00:00.2385835 

.NET/for:     00:00:01.7463579
.NET/initblk: 00:00:00.5953503

所以值得考虑。请注意,生成的 IL 将无法验证。

于 2014-09-12T13:01:22.133 回答
23

基于Lucero 的回答,这里有一个更快的版本。它将使用Buffer.BlockCopy每次迭代复制的字节数加倍。有趣的是,当使用相对较小的数组 (1000) 时,它的性能要高出 10 倍,但对于较大的数组 (1000000),差异并不大,但它总是更快。它的好处是它甚至可以在小型阵列中表现良好。在长度 = 100 左右,它比简单方法更快。对于一百万个元素字节数组,它快 43 倍。(在 Intel i7、.Net 2.0 上测试)

public static void MemSet(byte[] array, byte value) {
    if (array == null) {
        throw new ArgumentNullException("array");
    }

    int block = 32, index = 0;
    int length = Math.Min(block, array.Length);

    //Fill the initial array
    while (index < length) {
        array[index++] = value;
    }

    length = array.Length;
    while (index < length) {
        Buffer.BlockCopy(array, 0, array, index, Math.Min(block, length-index));
        index += block;
        block *= 2;
    }
}
于 2012-12-10T17:29:05.173 回答
22

有点晚了,但下面的方法可能是一个很好的折衷方案,而不会恢复到不安全的代码。基本上,它使用常规循环初始化数组的开头,然后恢复为Buffer.BlockCopy(),这应该与使用托管调用一样快。

public static void MemSet(byte[] array, byte value) {
  if (array == null) {
    throw new ArgumentNullException("array");
  }
  const int blockSize = 4096; // bigger may be better to a certain extent
  int index = 0;
  int length = Math.Min(blockSize, array.Length);
  while (index < length) {
    array[index++] = value;
  }
  length = array.Length;
  while (index < length) {
    Buffer.BlockCopy(array, 0, array, index, Math.Min(blockSize, length-index));
    index += blockSize;
  }
}
于 2010-03-25T19:40:22.203 回答
16

看起来现在与康拉德的回答提到System.Runtime.CompilerServices.Unsafe.InitBlock的指令做同样的事情(他还提到了一个源链接)。OpCodes.Initblk

填充数组的代码如下:

byte[] a = new byte[N];
byte valueToFill = 255;

System.Runtime.CompilerServices.Unsafe.InitBlock(ref a[0], valueToFill, (uint) a.Length);
于 2018-01-08T14:54:26.330 回答
14

这个简单的实现使用连续加倍,并且性能非常好(根据我的基准测试,比原始版本快大约 3-4 倍):

public static void Memset<T>(T[] array, T elem) 
{
    int length = array.Length;
    if (length == 0) return;
    array[0] = elem;
    int count;
    for (count = 1; count <= length/2; count*=2)
        Array.Copy(array, 0, array, count, count);
    Array.Copy(array, 0, array, count, length - count);
}

编辑:在阅读其他答案后,似乎我不是唯一一个有这个想法的人。不过,我还是把它留在这里,因为它更干净一些,而且性能与其他产品不相上下。

于 2013-09-06T13:47:54.573 回答
12

如果性能很关键,您可以考虑使用不安全代码并直接使用指向数组的指针。

另一种选择可能是从 msvcrt.dll 导入 memset 并使用它。但是,调用它的开销可能很容易大于速度的增益。

于 2009-12-13T20:06:51.707 回答
10

或使用 P/Invoke 方式

[DllImport("msvcrt.dll", 
EntryPoint = "memset", 
CallingConvention = CallingConvention.Cdecl, 
SetLastError = false)]
public static extern IntPtr MemSet(IntPtr dest, int c, int count);

static void Main(string[] args)
{
    byte[] arr = new byte[3];
    GCHandle gch = GCHandle.Alloc(arr, GCHandleType.Pinned);
    MemSet(gch.AddrOfPinnedObject(), 0x7, arr.Length); 
}
于 2013-09-27T21:44:58.510 回答
7

如果性能绝对至关重要,那么Enumerable.Repeat(n, m).ToArray()对于您的需求来说太慢了。您也许可以使用 PLINQ 或Task Parallel Library获得更快的性能:

using System.Threading.Tasks;

// ...

byte initialValue = 20;
byte[] data = new byte[size]
Parallel.For(0, size, index => data[index] = initialValue);
于 2009-12-14T16:25:56.693 回答
6

所有答案都只写单个字节 - 如果你想用单词填充字节数组怎么办?还是漂浮?我时不时地找到它的用途。因此,在以非通用方式多次编写了与“memset”类似的代码并到达此页面以找到适合单字节的代码之后,我开始编写以下方法。

我认为 PInvoke 和 C++/CLI 各有其缺点。为什么不在 mscorxxx 中为您提供运行时“PInvoke”?Array.Copy 和 Buffer.BlockCopy 当然是原生代码。BlockCopy 甚至不是“安全的”——只要它们在数组中,您就可以在另一个或 DateTime 上复制很长的一半。

至少我不会为这样的事情提交新的 C++ 项目——这几乎可以肯定是浪费时间。

所以这里基本上是 Lucero 和 TowerOfBricks 提出的解决方案的扩展版本,可用于 memset long、int 等以及单字节。

public static class MemsetExtensions
{
    static void MemsetPrivate(this byte[] buffer, byte[] value, int offset, int length) {
        var shift = 0;
        for (; shift < 32; shift++)
            if (value.Length == 1 << shift)
                break;
        if (shift == 32 || value.Length != 1 << shift)
            throw new ArgumentException(
                "The source array must have a length that is a power of two and be shorter than 4GB.", "value");

        int remainder;
        int count = Math.DivRem(length, value.Length, out remainder);

        var si = 0;
        var di = offset;
        int cx;
        if (count < 1) 
            cx = remainder;
        else 
            cx = value.Length;
        Buffer.BlockCopy(value, si, buffer, di, cx);
        if (cx == remainder)
            return;

        var cachetrash = Math.Max(12, shift); // 1 << 12 == 4096
        si = di;
        di += cx;
        var dx = offset + length;
        // doubling up to 1 << cachetrash bytes i.e. 2^12 or value.Length whichever is larger
        for (var al = shift; al <= cachetrash && di + (cx = 1 << al) < dx; al++) {
            Buffer.BlockCopy(buffer, si, buffer, di, cx);
            di += cx;
        }
        // cx bytes as long as it fits
        for (; di + cx <= dx; di += cx)
            Buffer.BlockCopy(buffer, si, buffer, di, cx);
        // tail part if less than cx bytes
        if (di < dx)
            Buffer.BlockCopy(buffer, si, buffer, di, dx - di);
    }
}

有了这个,你可以简单地添加简短的方法来获取你需要的值类型并调用私有方法,例如在这个方法中找到替换 ulong:

    public static void Memset(this byte[] buffer, ulong value, int offset, int count) {
        var sourceArray = BitConverter.GetBytes(value);
        MemsetPrivate(buffer, sourceArray, offset, sizeof(ulong) * count);
    }

或者傻傻地用任何类型的结构来做(尽管上面的 MemsetPrivate 只适用于编组为 2 次幂大小的结构):

    public static void Memset<T>(this byte[] buffer, T value, int offset, int count) where T : struct {
        var size = Marshal.SizeOf<T>();
        var ptr = Marshal.AllocHGlobal(size);
        var sourceArray = new byte[size];
        try {
            Marshal.StructureToPtr<T>(value, ptr, false);
            Marshal.Copy(ptr, sourceArray, 0, size);
        } finally {
            Marshal.FreeHGlobal(ptr);
        }
        MemsetPrivate(buffer, sourceArray, offset, count * size);
    }

我更改了前面提到的 initblk 以使用 ulongs 来比较我的代码的性能,并且默默地失败了 - 代码运行但生成的缓冲区仅包含 ulong 的最低有效字节。

尽管如此,我还是将性能写入作为大缓冲区与 for、initblk 和我的 memset 方法进行了比较。时间以毫秒为单位,总共超过 100 次重复写入 8 字节 ulong,无论多少次适合缓冲区长度。对于单个 ulong 的 8 个字节,for 版本是手动循环展开的。

Buffer Len  #repeat  For millisec  Initblk millisec   Memset millisec
0x00000008  100      For   0,0032  Initblk   0,0107   Memset   0,0052
0x00000010  100      For   0,0037  Initblk   0,0102   Memset   0,0039
0x00000020  100      For   0,0032  Initblk   0,0106   Memset   0,0050
0x00000040  100      For   0,0053  Initblk   0,0121   Memset   0,0106
0x00000080  100      For   0,0097  Initblk   0,0121   Memset   0,0091
0x00000100  100      For   0,0179  Initblk   0,0122   Memset   0,0102
0x00000200  100      For   0,0384  Initblk   0,0123   Memset   0,0126
0x00000400  100      For   0,0789  Initblk   0,0130   Memset   0,0189
0x00000800  100      For   0,1357  Initblk   0,0153   Memset   0,0170
0x00001000  100      For   0,2811  Initblk   0,0167   Memset   0,0221
0x00002000  100      For   0,5519  Initblk   0,0278   Memset   0,0274
0x00004000  100      For   1,1100  Initblk   0,0329   Memset   0,0383
0x00008000  100      For   2,2332  Initblk   0,0827   Memset   0,0864
0x00010000  100      For   4,4407  Initblk   0,1551   Memset   0,1602
0x00020000  100      For   9,1331  Initblk   0,2768   Memset   0,3044
0x00040000  100      For  18,2497  Initblk   0,5500   Memset   0,5901
0x00080000  100      For  35,8650  Initblk   1,1236   Memset   1,5762
0x00100000  100      For  71,6806  Initblk   2,2836   Memset   3,2323
0x00200000  100      For  77,8086  Initblk   2,1991   Memset   3,0144
0x00400000  100      For 131,2923  Initblk   4,7837   Memset   6,8505
0x00800000  100      For 263,2917  Initblk  16,1354   Memset  33,3719

我每次都排除了第一次通话,因为 initblk 和 memset 都受到了打击,我相信第一次通话大约是 0.22 毫秒。有点令人惊讶的是,我的代码填充短缓冲区比 initblk 更快,因为它有半页的设置代码。

如果有人想优化这个,那就继续吧。这是可能的。

于 2015-03-16T15:57:58.070 回答
5

随着Span<T>(这只是 dotnet 核心,但它是 dotnet 的未来)的出现,您还有另一种解决此问题的方法:

var array = new byte[100];
var span = new Span<byte>(array);

span.Fill(255);
于 2020-04-14T09:42:00.030 回答
4

测试了几种方法,在不同的答案中描述。在 c#测试类中查看测试源

基准报告

于 2016-03-14T19:16:38.770 回答
3

您可以在初始化数组时执行此操作,但我认为这不是您所要求的:

byte[] myBytes = new byte[5] { 1, 1, 1, 1, 1};
于 2009-12-13T20:02:54.040 回答
1

.NET Core 有一个内置的 Array.Fill() 函数,但遗憾的是 .NET Framework 缺少它。.NET Core 有两种变体:填充整个数组和从索引开始填充数组的一部分。

基于上述想法,这里有一个更通用的 Fill 函数,它将填充多个数据类型的整个数组。这是与本文中讨论的其他方法进行基准测试时最快的功能。

此函数以及填充数组部分的版本可在开源和免费的 NuGet 包(nuget.org 上的 HPCsharp)中获得。还包括使用 SIMD/SSE 指令的 Fill 的稍快版本,该指令仅执行内存写入,而基于 BlockCopy 的方法执行内存读取和写入。

    public static void FillUsingBlockCopy<T>(this T[] array, T value) where T : struct
    {
        int numBytesInItem = 0;
        if (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte))
            numBytesInItem = 1;
        else if (typeof(T) == typeof(ushort) || typeof(T) != typeof(short))
            numBytesInItem = 2;
        else if (typeof(T) == typeof(uint) || typeof(T) != typeof(int))
            numBytesInItem = 4;
        else if (typeof(T) == typeof(ulong) || typeof(T) != typeof(long))
            numBytesInItem = 8;
        else
            throw new ArgumentException(string.Format("Type '{0}' is unsupported.", typeof(T).ToString()));

        int block = 32, index = 0;
        int endIndex = Math.Min(block, array.Length);

        while (index < endIndex)          // Fill the initial block
            array[index++] = value;

        endIndex = array.Length;
        for (; index < endIndex; index += block, block *= 2)
        {
            int actualBlockSize = Math.Min(block, endIndex - index);
            Buffer.BlockCopy(array, 0, array, index * numBytesInItem, actualBlockSize * numBytesInItem);
        }
    }
于 2019-01-20T17:19:18.510 回答
0

大多数答案是针对 byte memset 但如果您想将它用于 float 或任何其他结构,您应该将索引乘以数据的大小。因为 Buffer.BlockCopy 将根据字节进行复制。此代码适用于浮点值

public static void MemSet(float[] array, float value) {
    if (array == null) {
        throw new ArgumentNullException("array");
    }

    int block = 32, index = 0;
    int length = Math.Min(block, array.Length);

    //Fill the initial array
    while (index < length) {
        array[index++] = value;
    }

    length = array.Length;
    while (index < length) {
        Buffer.BlockCopy(array, 0, array, index * sizeof(float), Math.Min(block, length-index)* sizeof(float));
        index += block;
        block *= 2;
    }
}
于 2020-10-26T14:41:42.407 回答
-1

Array 对象有一个名为 Clear 的方法。我敢打赌,Clear 方法比您用 C# 编写的任何代码都要快。

于 2018-03-09T16:16:11.260 回答