【问题标题】:What is the fastest way to Initialize a multi-dimensional array to non-default values in .NET?在 .NET 中将多维数组初始化为非默认值的最快方法是什么?
【发布时间】:2010-05-04 02:19:54
【问题描述】:

如何尽快初始化原始类型的多维数组?

我一直坚持使用多维数组。我的问题是性能。以下例程在大约初始化一个 100x100 数组。 500 滴答声。删除 int.MaxValue 初始化会导致大约。仅用于循环的 180 个滴答声。大约 100 个滴答来创建数组,无需循环且无需初始化为 int.MaxValue。

  • 在“运行”期间,与此类似的例程被调用几十万到几百万次。
  • 数组大小在运行期间不会改变,数组是一次创建一个,使用,然后丢弃,并创建一个新数组。
  • “运行”可能会持续一分钟(使用 10x10 阵列)到四十五分钟 (100x100)。
  • 应用程序创建 int、bool 和 struct 数组。
  • 可以同时执行多个“运行”,但不是因为性能严重下降。
  • 我使用 100x100 作为基线。

我愿意接受有关如何优化数组的这种非默认初始化的建议。我的一个想法是在可用时使用较小的原始类型。例如,使用 byte 而不是 int,可以节省 100 个刻度。我会对此感到满意,但我希望不必更改原始数据类型。

    public int[,] CreateArray(Size size) {
        int[,] array = new int[size.Width, size.Height];
        for (int x = 0; x < size.Width; x++) {
            for (int y = 0; y < size.Height; y++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

以下是 450 个滴答声:

    public int[,] CreateArray1(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        for (int x = 0; x < iX; x++) {
            for (int y = 0; y < iY; y++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

创建数组5;接受的实施:限制:无法调整大小,可以更改

在一次性初始化 2800 个刻度后,减少到大约 165 个刻度。 (请参阅下面的答案。)如果我可以让stackalloc 处理多维数组,我应该能够获得相同的性能而无需初始化private static 数组。

    private static bool _arrayInitialized5;
    private static int[,] _array5;

    public static int[,] CreateArray5(Size size) {
        if (!_arrayInitialized5) {
            int iX = size.Width;
            int iY = size.Height;
            _array5 = new int[iX, iY];
            for (int x = 0; x < iX; x++) {
                for (int y = 0; y < iY; y++) {
                    _array5[x, y] = int.MaxValue;
                }
            }
            _arrayInitialized5 = true;
        }
        return (int[,])_array5.Clone();
    }

创建数组8;接受的实施;限制:需要完全信任

在不使用上面的“克隆技术”的情况下,可以减少到大约 165 个滴答声。 (请参阅下面的答案。)如果我能弄清楚CreateArray9 的返回值,我相信我可以降低滴答声。

    public unsafe static int[,] CreateArray8(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        fixed (int* pfixed = array) {
            int count = array.Length;
            for (int* p = pfixed; count-- > 0; p++)
                *p = int.MaxValue;
        }
        return array;
    }

注意事项

我将提供有关此问题的所有代码和注释作为答案。希望它会在未来节省一些人的时间。

在大对象堆 (LOH) 上分配的数组不在此讨论范围内。提到的性能改进仅适用于在堆上分配的数组。

堆栈分配

我使用stackalloc 来消除将数组初始化为默认值的想法没有成功,因为分配的堆栈内存必须从方法中复制出来。意思是,我必须创建另一个数组来保存结果。该数组将被初始化,从而破坏了使用 stackalloc 的全部目的。

创建数组8;不安全/固定的方法

如果 unsafe 代码位于完全受信任的程序集中,CLR 只会执行它。

创建数组5;克隆方法

需要变量来确定数组是否已初始化并存储已初始化的数组。性能与初始化后的 unsafe/fixed 方法相同。有关可能的解决方案,请参阅 Dan Tao 的答案。

300% 性能提升?

百分比我很烂,但我认为是 300%(500 到 165 个滴答声)。


应用的最终实现

对于这个应用程序,我决定使用“克隆”方法。以下是应用程序中使用的“精益”通用实现以及性能示例。

初始化:

  • Grid&lt;int&gt;;通用克隆类初始化:4348、4336、4339、4654
  • Grid&lt;bool&gt;;通用克隆类初始化:2692、2684、3916、2680
  • Grid&lt;Color&gt;;通用克隆类初始化:3747、4630、2702、2708

用途:

  • Grid&lt;int&gt;;通用克隆类:185、159、152、290
  • Grid&lt;bool&gt;;通用克隆类:39、36、44、46
  • Grid&lt;Color&gt;;通用克隆类:2229、2431、2460、2496

    public class Grid<T> {
        private T[,] _array;
        private T _value;
        private bool _initialized;
        private int _x;
        private int _y;
        public Grid(Size size, T value, bool initialize) {
            _x = size.Width;
            _y = size.Height;
            _value = value;
            if (initialize) {
                InitializeArray();
            }
        }
        private void InitializeArray() {
            int iX = _x;
            int iY = _y;
            _array = new T[iX, iY];
            for (int y = 0; y < iY; y++) {
                for (int x = 0; x < iX; x++) {
                    _array[x, y] = _value;
                }
            }
            _initialized = true;
        }
        public T[,] CreateArray() {
            if (!_initialized) {
                InitializeArray();
            }
            return (T[,])_array.Clone();
        }
    }
    

【问题讨论】:

  • 愚蠢的问题:为什么需要将每个槽初始化为int.MaxValue
  • @丹涛; -1 表示愚蠢的评论。
  • @AMissico:哈,等等,我想你误会了。我的意思是我的问题是愚蠢的,而不是你的!
  • @丹涛; +1 为良好的复出。 :O)
  • 未记录的数组特征;这里有一些有趣的优化,比如使用锯齿状而不是矩形数组来减少内部 CLR 检查 - codeproject.com/KB/dotnet/arrays.aspx

标签: .net initialization multidimensional-array


【解决方案1】:

关于您的Clone 方法的说明:我怀疑您能否在性能方面击败它。但是,考虑到在第一次初始化之后,它会忽略Size 参数,并且在每次调用时只返回一个相同大小的数组,这可能是一个重大更改。根据这在您的场景中是否真正重要,您可以:

  1. 坚持下去,因为没关系。
  2. 创建一个Dictionary&lt;Size, int[,]&gt;(我相信Size 可以作为一个键正常运行——尚未测试)以在每次请求唯一的Size 时预初始化一个数组。我不确定它的开销。
  3. 放弃Clone 的想法。

如果你最终不得不选择上面的 3,这里有一些荒谬的建议:

1.在本地缓存您的 WidthHeight 属性,而不是在每次迭代时从 Size 结构访问它们

static int[,] CreateArray(Size size) {
    int w = size.Width;
    int h = size.Height;

    int[,] array = new int[w, h];
    for (int x = 0; x < w; x++) {
        for (int y = 0; y < h; y++) {
            array[x, y] = int.MaxValue;
        }
    }

    return array;
}

要在我的机器上创建一个 1000x1000 的数组,这导致平均执行时间约为 120000 滴答,而平均执行时间约为 140000 滴答。

2。如果您有多个内核,请利用多个内核并并行初始化阵列。

static int[,] CreateArray(Size size) {
    int w = size.Width;
    int h = size.Height;

    int[,] array = new int[w, h];
    Action<int[,], int, int> fillFirstHalf = FillArray;
    Action<int[,], int, int> fillSecondHalf = FillArray;

    var firstResult = fillFirstHalf.BeginInvoke(array, 0, h / 2, null, null);
    var secondResult = fillSecondHalf.BeginInvoke(array, h / 2, h, null, null);

    fillFirstHalf.EndInvoke(firstResult);
    fillSecondHalf.EndInvoke(secondResult);

    return array;
}

static void FillArray(int[,] array, int ystart, int yend) {
    int w = array.GetLength(0);

    for (int x = 0; x < w; ++x) {
        for (int y = ystart; y < yend; ++y) {
            array[x, y] = int.MaxValue;
        }
    }
}

在您的场景中,这可能不是一个非常现实的建议,因为您似乎只创建 100x100 数组,在这种情况下,并行化的开销超过了性能增益。然而,对于创建一个 1000x1000 数组,我发现这种方法将我的执行时间平均减少到了大约 70k 滴答声(与我建议的第一次优化得到的约 120k 滴答声相比)。

另外,如果您以这种方式创建 许多数组,我强烈建议您并行化 that(即,如果您需要创建一千个数组,则每个从两个线程),假设您有多个处理器为您完成工作。没有多个处理器,算了;添加线程只会损害您的性能。

3.使用unsafe 指针获得增强的性能。

现在这里有一个有趣的发现:似乎 .NET 中的二维数组以可预测的方式分配*:基本上作为一维内存块,其中每个“行”从起点偏移等于所有先前行的长度的量。换句话说,可以使用指针访问 10x2 数组,就像 20x1 数组一样; 10x10 数组可以像 100x1 数组一样被访问,等等。

我不知道这是否是记录在案的行为。它可能是您不想依赖的未指定的实现细节。无论哪种方式,都值得研究。

* 可能大多数其他 .NET 开发人员已经知道这一点,我只是在说明显而易见的情况,在这种情况下,我撤销关于这“有趣”的评论。

无论如何,了解这一点后,您就可以在 unsafe 上下文中利用 fixed 关键字来显着提高性能:

static int[,] CreateArray(Size size) {
    int w = size.Width;
    int h = size.Height;

    int[,] array = new int[w, h];
    unsafe {
        fixed (int* ptr = array) {
            for (int i = 0; i < w * h; ++i)
                ptr[i] = int.MaxValue;
        }
    }

    return array;
}

为了初始化一个显着大小的数组,我什至建议将上述方法(并行化)与这个方法结合起来——所以,保持与建议 #2 相同的CreateArray,然后将FillArray 重写为:

static void FillArray(int[,] array, int ystart, int yend) {
    int w = array.GetLength(0);

    unsafe {
        fixed (int* p = array) {
            for (int i = w * ystart; i < w * yend; ++i)
                p[i] = int.MaxValue;
        }
    } 
}

实际上,在我发布此内容之前,您似乎已经弄清楚了最后一部分,但我想我还是将其包括在内,主要是为了将unsafe 与并行化结合起来。


stackalloc 上的注释:我想你可能是在用这个追逐彩虹尽头的妖精。根据the documentation on stackalloc

足够大的内存块 包含expr 类型为type 的元素 分配在堆栈上,而不是 堆;该块的地址是 存储在指针ptr 中。这段记忆是 不受垃圾收集和 因此不必固定 (通过fixed)。 生命周期 内存块仅限于 它所在方法的生命周期 已定义。(强调我的)

这让我相信你不能 返回一个对象,其数据存储在stackalloc从函数分配的内存中,因为该内存仅分配给函数的生命周期。

【讨论】:

  • 我写了一篇关于积累大量微优化和提高性能的博客,想你可能会喜欢,akashkava.com/blog/18/c-optimizations-via-caching
  • 谢谢,我应该抓住这个。为 int[100,100] 节省 50 个刻度。更新了答案。
  • 在“运行”期间按顺序创建数组。可以同时执行多个“运行”。数组大小在“运行”期间不会改变。我使用 100x100 作为基线。在这种大小下,应用程序需要 45 分钟,而 10x10 需要 1 分钟。
  • 我使用了静态数组和静态标志,因为我无法让 stackalloc 工作。如果我可以让 stackalloc 工作,那么就不需要它们了。
  • @AMissico:根据我对stackalloc 的理解,它根本不可能用于多维数组,原因很简单,stackalloc 通过分配一块内存并返回一个pointer -- 如您所知,从该块中访问项目通过提供与指针的偏移量来工作。从 pointer 访问一个项目到 multidimensional 数组(如果可能的话)将需要 两个 偏移量。也就是说,您可以stackalloc 用于锯齿状数组,我相信无论如何它具有更好的性能。但这肯定需要一些重构。
【解决方案2】:

用于粒度控制的非托管数组

在 C# 非托管 (unsafe) 模式下创建数组,如 shown here[code project],并手动初始化。

在 C# 托管模式下,数组首先将所有元素初始化为默认值,然后循环开始填充它。您可能会在非托管模式下减少加倍。这样可以节省很多时间。

【讨论】:

  • 好文章。然而,我认为与我添加到我的问题中的 CreateArray8 相比,我不会看到任何性能提升。
  • “在 C# 托管模式下,数组首先将所有元素初始化为默认值,然后再开始循环”是的。您的陈述使我相信我可以使用 stackalloc 停止此初始化。
  • @AMisscio:确实Marshal.AllocHGlobal 导致分配的内存被锁定到位。此外,分配的内存不是零填充的。 参考:msdn.microsoft.com/en-us/library/s69bkh17.aspx 但是我迷失在所需的实现细节中,还没有用基于此的可行的非托管代码解决方案更新我的答案。删除数组上的默认 CLR 初始化肯定会加快速度并将初始化值留给您 - 在这种情况下是 Int32.MaxValue。
  • ... 并且可能还有其他非托管代码方法可以放弃初始化阶段。我在这种模式下工作的经验不是很丰富。然而,这些信息可能会导致一些真正的优化解决方案。
  • 要补充这个答案,请参阅codeproject.com/KB/dotnet/Large_Byte_Array_handling.aspx 的“大字节数组的处理”。它提供了一些性能测量和其他行为。
【解决方案3】:

添加staticunsafe 可以减少Ticks。以下是一些示例。

  • 创建数组;非静态非不安全:521、464、453、474
  • 创建数组;静态:430、423、418、454
  • 创建数组;不安全:485、464、435、414
  • 创建数组;静态不安全:476、450、433、405

我尝试使用stackalloc。我的想法是分配数组,它不会被初始化,因为它是unsafe 代码。然后我会压缩数组,初始化为int.MaxValue,然后Clone 数组作为返回结果。但是,我对多维声明感到困惑。

然后我记得在另一个项目中的数组上使用Clone。每个Array.Clone 节省了几秒钟。基于这个想法,我创建了以下版本的CreateArray 例程,得到了很好的结果。

现在,我需要做的就是让stackalloc 处理多维数组。

  • 创建数组5;预初始化:2663、3036
  • 创建数组5;克隆:157、172

    private static bool _arrayInitialized5;
    private static int[,] _array5;
    
    public static int[,] CreateArray5(Size size) {
        if (!_arrayInitialized5) {
            int iX = size.Width;
            int iY = size.Height;
            _array5 = new int[iX, iY];
            for (int x = 0; x < iX; x++) {
                for (int y = 0; y < iY; y++) {
                    _array5[x, y] = int.MaxValue;
                }
            }
            _arrayInitialized5 = true;
        }
        return (int[,])_array5.Clone();
    }
    

    int[,] actual;

    int iHi = 10000 * 10 * 2; 
    //'absolute minimum times array will be created   (200,000)
    //'could be as high as 10000 * 10 * 20? * 50? (100,000,000?)

    Stopwatch o;

    //'pre-initialize
    o = Stopwatch.StartNew();
    actual = CreateArray5(new Size(100, 100));
    o.Stop();
    Trace.WriteLine(o.ElapsedTicks, "CreateArray5; pre-initialize");
    o = Stopwatch.StartNew();
    for (int i = 0; i < iHi; i++) { actual = CreateArray5(new Size(100, 100)); }
    o.Stop();
    Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray5; static unsafe clone");

【讨论】:

    【解决方案4】:

    这在这种情况下未经测试,但在类似的情况下也有效。 有时,由于内存局部性,交换数组遍历的顺序会加快速度。

    换句话说,不是for(x) ... for(y),而是for(y) ... for(x)

    【讨论】:

    • 在这种情况下,它始终可以节省大约。 20 滴答声。
    • 好吧,给你。每个滴答声都很重要。
    • 上一次我担心这样的滴答声是为 Windows CE 2.0 开发的,因为处理器能力有限。由于迭代次数巨大,这是第一次。 :O)
    【解决方案5】:

    你可以使用并行库来并行使用每一行来初始化它,它会更快。

    但是,我认为这个 For 有限制,只能排队 64 个操作,但在这种情况下,您可以在 Parallel.For 中排队 0 到 63、64 到 127 等..

    public int[,] CreateArray(Size size) { 
        int[,] array = new int[size.Width, size.Height]; 
        System.Threading.Paralle.For (0,size.Width, 
          x=>{ 
            for (int y = 0; y < size.Height; y++) { 
                array[x, y] = int.MaxValue; 
            } 
          }
        ); 
        return array; 
    } 
    

    这包含在 .NET 4 中,但是对于 .NET 3.51,您可以从 codeplex 获得相同的库。

    【讨论】:

      【解决方案6】:

      我能够将滴答声降低到大约 165。请参阅下面的 CreateArray8

      我从 jdk 提供的 http://www.codeproject.com/KB/dotnet/arrays.aspx 的 CodeProject 文章的“范围检查消除”部分得到了一个想法。 (@jdk,非常感谢。)这个想法是通过使用指针并在一个循环中初始化每个元素来消除范围检查。我能够将滴答声降低到大约 165。就像克隆一样好,没有预初始化和支持静态变量的延迟。 (请参阅我的其他答案。)

      如果我能算出CreateArray9 的返回值,我敢打赌我可以把它减半。

      • 创建数组3;静态不安全:501、462、464、462
      • 创建数组7;对于(y,x)静态不安全:452、451、444、429
      • 创建数组8;静态不安全指针 single_for: 187, 173, 156, 145

      [TestClass]
      public class CreateArrayTest {
      
          public static unsafe int[,] CreateArray3(Size size) {
              int iX = size.Width;
              int iY = size.Height;
              int[,] array = new int[iX, iY];
              for (int x = 0; x < iX; x++) {
                  for (int y = 0; y < iY; y++) {
                      array[x, y] = int.MaxValue;
                  }
              }
              return array;
          }
      
          public unsafe static int[,] CreateArray7(Size size) {
              int iX = size.Width;
              int iY = size.Height;
              int[,] array = new int[iX, iY];
              for (int y = 0; y < iY; y++) {
                  for (int x = 0; x < iX; x++) {
                      array[x, y] = int.MaxValue;
                  }
              }
              return array;
          }
      
          public unsafe static int[,] CreateArray8(Size size) {
              int iX = size.Width;
              int iY = size.Height;
              int[,] array = new int[iX, iY];
              fixed (int* pfixed = array) {
                  int count = array.Length;
                  for (int* p = pfixed; count-- > 0; p++)
                      *p = int.MaxValue;
              }
              return array;
          }
      
          public unsafe static int[,] CreateArray9(Size size) {
              int iX = size.Width;
              int iY = size.Height;
              void* array = stackalloc int[iX * iY];
              int count = iX * iY;
              for (int* p = (int*)array; count-- > 0; p++)
                  *p = int.MaxValue;
      
              //return (int[,])array; //how to return?
              return new int[1, 1];
          }
      
          [TestMethod()]
          public void CreateArray_Test() {
      
              int[,] actual;
      
              int iHi = 10000 * 10 * 2;
              //'absolute minimum times array will be created   (200,000)
              //'could be as high as 10000 * 10 * 20? * 50? (100,000,000?)
      
              Stopwatch o;
      
              o = Stopwatch.StartNew();
              for (int i = 0; i < iHi; i++) { actual = CreateArray3(new Size(100, 100)); }
              o.Stop();
              Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray3; static unsafe");
      
              o = Stopwatch.StartNew();
              for (int i = 0; i < iHi; i++) { actual = CreateArray7(new Size(100, 100)); }
              o.Stop();
              Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray7; static unsafe for(y,x)");
      
              o = Stopwatch.StartNew();
              for (int i = 0; i < iHi; i++) { actual = CreateArray8(new Size(100, 100)); }
              o.Stop();
              Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray8; static unsafe pointer single_for");
      
          }
      
      }
      

      【讨论】:

        【解决方案7】:

        类和泛型使用“克隆”方法。

        • MD 阵列;克隆类初始化:2444、2587、2421、2406
        • MD 阵列;克隆类:440、362、198、139
        • Grid&lt;int&gt;;通用克隆类初始化:5344、5334、5693、5272
        • Grid&lt;int&gt;;通用克隆类:187、204、199、288
        • Grid&lt;bool&gt;;通用克隆类初始化:3585、3537、3552、3569
        • Grid&lt;bool&gt;;通用克隆类:37、44、36、43
        • Grid&lt;Color&gt;;通用克隆类初始化:4139、3536、3503、3533
        • Grid&lt;Color&gt;;通用克隆类:2737、3137、2414、2171

        [TestClass]
        public class CreateArrayTest {
        
            public class MDArray {
                private bool _initialized;
                private int[,] _array;
                private int _x;
                private int _y;
                private int _value;
                public MDArray(Size size, int value, bool initialize) {
                    _x = size.Width;
                    _y = size.Height;
                    _value = value;
                    if (initialize) {
                        InitializeArray();
                    }
                }
                private void InitializeArray() {
                    int iX = _x;
                    int iY = _y;
                    _array = new int[iX, iY];
                    for (int y = 0; y < iY; y++) {
                        for (int x = 0; x < iX; x++) {
                            _array[x, y] = _value;
                        }
                    }
                    _initialized = true;
                }
                public int[,] CreateArray() {
                    if (!_initialized) {
                        InitializeArray();
                    }
                    return (int[,])_array.Clone();
                }
            }
        
            public class Grid<T> {
                private T[,] _array;
                private T _value;
                private bool _initialized;
                private int _x;
                private int _y;
                public Grid(Size size, T value, bool initialize) {
                    _x = size.Width;
                    _y = size.Height;
                    _value = value;
                    if (initialize) {
                        InitializeArray();
                    }
                }
                private void InitializeArray() {
                    int iX = _x;
                    int iY = _y;
                    _array = new T[iX, iY];
                    for (int y = 0; y < iY; y++) {
                        for (int x = 0; x < iX; x++) {
                            _array[x, y] = _value;
                        }
                    }
                    _initialized = true;
                }
                public T[,] CreateArray() {
                    if (!_initialized) {
                        InitializeArray();
                    }
                    return (T[,])_array.Clone();
                }
            }
        
            [TestMethod()]
            public void CreateArray_Test() {
        
                int[,] actual;
        
                int iHi = 10000 * 10 * 2; //'absolute minimum times array will be created   (200,000)
                //                          'could be as high as 10000 * 10 * 20? * 50? (100,000,000?)
        
                Stopwatch o;
        
                o = Stopwatch.StartNew();
                MDArray oMDArray = new MDArray(new Size(100, 100), int.MaxValue, true);
                o.Stop();
                Trace.WriteLine(o.ElapsedTicks, "     MDArray; clone class initalize");
                o = Stopwatch.StartNew();
                for (int i = 0; i < iHi; i++) { actual = oMDArray.CreateArray(); }
                o.Stop();
                Trace.WriteLine(o.ElapsedTicks / iHi, "     MDArray; clone class");
        
                o = Stopwatch.StartNew();
                Grid<int> oIntMap = new Grid<int>(new Size(100, 100), int.MaxValue, true);
                o.Stop();
                Trace.WriteLine(o.ElapsedTicks, "   Grid<int>; generic clone class initalize");
                o = Stopwatch.StartNew();
                for (int i = 0; i < iHi; i++) { actual = oIntMap.CreateArray(); }
                o.Stop();
                Trace.WriteLine(o.ElapsedTicks / iHi, "   Grid<int>; generic clone class");
        
                bool[,] fActual;
                o = Stopwatch.StartNew();
                Grid<bool> oBolMap = new Grid<bool>(new Size(100, 100), true, true);
                o.Stop();
                Trace.WriteLine(o.ElapsedTicks, "  Grid<bool>; generic clone class initalize");
                o = Stopwatch.StartNew();
                for (int i = 0; i < iHi; i++) { fActual = oBolMap.CreateArray(); }
                o.Stop();
                Trace.WriteLine(o.ElapsedTicks / iHi, "  Grid<bool>; generic clone class");
        
                Color[,] oActual;
                o = Stopwatch.StartNew();
                Grid<Color> oColMap = new Grid<Color>(new Size(100, 100), Color.AliceBlue, true);
                o.Stop();
                Trace.WriteLine(o.ElapsedTicks, " Grid<Color>; generic clone class initalize");
                o = Stopwatch.StartNew();
                for (int i = 0; i < iHi; i++) { oActual = oColMap.CreateArray(); }
                o.Stop();
                Trace.WriteLine(o.ElapsedTicks / iHi, " Grid<Color>; generic clone class");
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-03-13
          • 2011-03-26
          • 2022-11-17
          • 2011-01-19
          • 2020-06-21
          • 1970-01-01
          • 2014-02-22
          相关资源
          最近更新 更多