比较2字节数组

时间:2014-09-09 11:30:03

标签: c# arrays

我有2个int数组。

int[] data1 #
int[] data2 #

我想创建第3个int [] data3,这是其他2个数组之间的差异。

让我们取data1中的第一个值。

值为15(例如)。

现在让我们在data2中获取第一个值。

值为3(例如)。

data3中的第一个值为12。

但是,如果第一个值是相反的,即

data1[0]  = 3
data2[0]  = 15

那么差异将是-12。但我希望它只有12岁。

目前我有一个for循环,我在那里进行计算,得到那种结果。

  1. 有没有办法在不枚举的情况下执行data1-data2 = data3 一个循环?
  2. 如果是这样,我可以在不使用减号的情况下获得差异     号码?
  3. 由于

    N.B。 回应'闭门器'。我同意你的观点。我需要在这个问题上添加:

    我正在寻找最有效(最快的方式,但低内存是第二优先)来促进这一点。使用Linq(据我所知)可能是最慢的方法吗?

4 个答案:

答案 0 :(得分:8)

您正在寻找Zip方法

var data3 = data1.Zip(data2, (d1,d2) => Math.Abs(d1 - d2)).ToArray();

Enumerable.Zip<TFirst, TSecond, TResult> Method

  

将指定的函数应用于两个序列的相应元素,生成一系列结果。

所以它只需要每个对应的元素,例如data1[0]data2[0],然后是data1[1]data2[1]等......然后应用函数Math.Abs(d1-d2)简单地减去两个数字并获得结果的绝对值。然后返回一个包含每个操作结果的序列。

答案 1 :(得分:5)

&#34;有没有办法在不通过循环枚举的情况下执行data1-data2 = data3?&#34;不,这在技术上是不可能的。

充其量,或者更糟糕的是,你可以调用为你做枚举的函数。但它会很慢。在LINQ的情况下,不合时宜的。

对于我正在处理其他答案的结果的机器,对于4KB表(1024个整数),结果如下。

  • 23560 ticks - Giannis Paraskevopoulos。 Array-Enumerable-Array转换速度不太快,通过ToList()复制数组.ToArray()链比Array.Copy()慢大约25倍。
  • 10198蜱 - Selman22。快2倍,但仍然很慢。 Lambdas是令人眼花缭乱的创造活动更美观,而不是更快。你最终会得到一些匿名的方法,这可能会占用更多的CPU时间来进行回调而不是它的操作(记住我们在这里做的数学CPU可以在几个周期内完成)。
  • 566 ticks - Tim Schmelter GetDifference()函数(主要罪魁祸首是JIT,在本机代码和/或更常见的使用差异可以忽略不计)
  • 27个嘀嗒 - 只是一个循环。比Zip快400倍,比将阵列转换为列表和返回快800多。

循环代码:

for (int i = 0; i < data3.Length; i++)
{
  data3[i] = Math.Abs(data1[i] - data2[i]);
}

这样的基本内存操作可以直接转换为机器代码,而不会出现可怕的性能和LINQ的大量内存占用。

故事的道德是:LINQ的可读性(在这种情况下是有争议的)不是为了表现(在这种情况下是明显的)。


优化时间!让我们稍微滥用我们的CPU。

  1. Unroll loop。或者不。您的体验可能有所不同即使在 汇编程序本身循环展开性能增益或损失各不相同 在同一系列的处理器中。新的CPU和编译器是 意识到老技巧,只需自己实施。对于 i3-3220我在循环上测试代码展开到4行导致更快 在32位代码上执行但是在64位上执行它有点慢,而展开到8则相反。
  2. 编译x64。当我们在这里处理32位数据时,我们不会成功 使用64位寄存器......还是我们?在x86不到一半 寄存器真正可用于生成的代码(在汇编编写中 手动,你可以总是挤出更多),在x64但是你得到八个奖金登记册,可以免费使用。无需访问内存就能做得越多,代码就越快。在这种情况下,速度增益约为20%。
  3. 关闭Visual Studio。不要在32位IDE中快速测试64位代码 (现在没有64位版本,probably wont be for long time)。这将使x64代码大约慢两倍 到架构不匹配。 (嗯......你永远不应该在调试器下对代码进行速度测试......)
  4. 请勿过多使用内置功能。在这种情况下,Math.Abs​​有 overhead hidden inside。由于某些原因(需要分析IL才能找到),使用?:检查负值比使用If-Else更快。这样的检查节省了很多时间。
  5. UPDATE:?:由于生成的机器代码的差异而比If-Else快...至少只是比较两个值。它的机器代码远不如If-Else(它看起来不像你要写的那样#34;副手#34;)。显然,它不仅仅是编写If-Else语句的不同形式,而是针对简单条件赋值优化的完全独立命令。

    结果代码比使用Math.Abs​​()的简单循环快大约8倍;请记住,您只能将循环展开为数据集大小的除数。你写道你的数据集大小是25920,所以8很好。 (最大值是64,但我怀疑它会有多高的意义)。我建议将这段代码隐藏在某些函数中,因为它很难看。

    int[] data3 = new int[data1.Length];
    for (int i = 0; i < data1.Length; i += 8)
    {
        int b;
        b = (data1[i + 0] - data2[i + 0]);
        data3[i + 0] = b < 0 ? -b : b;
        b = (data1[i + 1] - data2[i + 1]);
        data3[i + 1] = b < 0 ? -b : b;
        b = (data1[i + 2] - data2[i + 2]);
        data3[i + 2] = b < 0 ? -b : b;
        b = (data1[i + 3] - data2[i + 3]);
        data3[i + 3] = b < 0 ? -b : b;
        b = (data1[i + 3] - data2[i + 4]);
        data3[i + 4] = b < 0 ? -b : b;
        b = (data1[i + 5] - data2[i + 5]);
        data3[i + 5] = b < 0 ? -b : b;
        b = (data1[i + 6] - data2[i + 6]);
        data3[i + 6] = b < 0 ? -b : b;
        b = (data1[i + 7] - data2[i + 7]);
        data3[i + 7] = b < 0 ? -b : b;
    }
    

    这甚至不是最终形式。我会尝试在它上面做一些异端技巧。

    BitHack,低级作弊!

    正如我所提到的,仍有改进的地方。

    在切断LINQ之后,主要的蜱munchkin是Abs()。当它从代码中删除时,我们在IF-ELSE和速记?:运算符之间留下了较量。 两者都是分支运算符,过去曾被广泛认为比线性代码慢。目前,易用性/写作倾向于挑选性能(有时正确,有时不正确)。

    因此,让我们的分支条件成为线性的。有可能通过滥用这个代码中的分支包含仅对单个变量进行操作的事实。因此,让代码等同于this

    现在你还记得如何否定两个补码?,否定所有位并加一个。让我们无条件地在一行中做到这一点!

    按位操作员有时间闪耀。 OR和AND很无聊,真正的男人使用异或。 XOR真的很酷吗?除了通常的行为之外,您还可以将其转换为NOT(否定)和NOP(无操作)。

    1 XOR 1 = 0
    0 XOR 1 = 1
    

    因此,仅按1和1的值进行XOR操作会使您无法操作。

    1 XOR 0 = 1
    0 XOR 0 = 0
    

    因此,只有0&#39;的XOR值根本没有任何作用。

    我们可以从我们的号码获取标志。对于32位整数,它就像x>>31一样简单。它将位符号移动到最低位。正如wiki会告诉你的那样,从左边插入的位将是零,因此x>>31的结果对于负数(x <0)将为1,对于非负数(x&gt; = 0)将为0,对吗?

    不。对于有符号值,Arithmetic shift用于普通位移。因此我们将根据符号获得-1或0 ....这意味着&#39; x&gt;&gt; 31&#39;将给出111 ... 111的负数和000 ... 000的非负数。如果您将根据此类移位的结果对原始x进行异或,则根据值符号执行NOT或NOP。另一个有用的东西是0将导致NOP用于加/否,所以我们可以根据值符号加/减-1。

    所以&#39; x ^(x&gt;&gt; 31)&#39;将翻转负数位而不对非负数和&#39; x-(x>&gt; 31)&#39;将负x加1(否定负值给出正数)并且不对非负值进行更改。

    合并后,你得到&#39;(x ^(x&gt;&gt; 31)) - (x&gt;&gt; 31)&#39; ...可以翻译成:

    IF X<0
      X=!X+1
    

    它只是

    IF X<0
      X=-X
    

    它如何影响性能? 我们的XorAbs()只需要四个基本的整数运算,一个加载和一个存储。分支运算符本​​身需要大约CPU额定值。虽然现代CPU非常擅长进行分支预测,但在提供顺序代码时,它们仍然可以更快地完成它。

    分数是什么?

    1. 比内置Abs()快4倍;
    2. 大约是以前代码的两倍(没有展开的版本)
    3. 根据CPU的不同,无需循环展开即可获得更好的结果。 由于消除了代码分支,CPU可以&#34;展开&#34;在它上面循环 拥有。 (Haswells在展开时很奇怪)
    4. 结果代码:

      for (int i = 0; i < data1.Length; i++)
      {
        int x = data1[i] - data2[i];
        data3[i] = (x ^ (x >> 31)) - (x >> 31);
      }
      

      并行和缓存使用

      CPU具有超高速缓存内存,当按顺序处理数组时,它会将整个数据块复制到缓存中。 但是如果你编写糟糕的代码,你会得到缓存未命中。您可以通过screwing up order of nested loops轻松陷入此陷阱。

      并行(多线程,相同数据)必须在顺序块上工作才能充分利用cpu缓存。

      手动编写线程允许您手动为线程选择块,但这很麻烦。 由于4.0 .NET附带了帮助程序,但是默认Parallel.For会导致缓存混乱。 因此,由于cache-miss,此代码实际上比单线程版本慢。

      Parallel.For(0, data1.Length,
      fn =>
      {
        int x = data1[fn] - data2[fn];
        data3[fn] = (x ^ (x >> 31)) - (x >> 31);
      }
      

      可以通过在其中执行顺序操作来手动使用缓存数据。例如,你可以展开循环,但它的脏黑客和展开有自己的性能问题(它取决于CPU模型)。

      Parallel.For(0, data1.Length >> 3,
      i =>
      {
          int b;
          b = (data1[i + 0] - data2[i + 0]);
          data3[i + 0] = b < 0 ? (b ^ -1) + b : b;
          b = (data1[i + 1] - data2[i + 1]);
          data3[i + 1] = b < 0 ? (b ^ -1) + b : b;
          b = (data1[i + 2] - data2[i + 2]);
          data3[i + 2] = b < 0 ? (b ^ -1) + b : b;
          b = (data1[i + 3] - data2[i + 3]);
          data3[i + 3] = b < 0 ? (b ^ -1) + b : b;
          b = (data1[i + 3] - data2[i + 4]);
          data3[i + 4] = b < 0 ? (b ^ -1) + b : b;
          b = (data1[i + 5] - data2[i + 5]);
          data3[i + 5] = b < 0 ? (b ^ -1) + b : b;
          b = (data1[i + 6] - data2[i + 6]);
          data3[i + 6] = b < 0 ? (b ^ -1) + b : b;
          b = (data1[i + 7] - data2[i + 7]);
          data3[i + 7] = b < 0 ? (b ^ -1) + b : b;
      }
      

      然而,.NET也有Parrarel.ForEachLoad Balancing Partitioners。 通过使用它们,你可以获得最好的世界:

      • 数据集大小独立代码
      • 简短,整洁的代码
      • 多线程
      • 良好的缓存使用率

      所以最终的代码是:

      var rangePartitioner = Partitioner.Create(0, data1.Length);
      Parallel.ForEach(rangePartitioner, (range, loopState)
      =>
      {
          for (int i = range.Item1; i < range.Item2; i++)
          {
              int x = data1[i] - data2[i];
              data3[i] = (x ^ (x >> 31)) - (x >> 31);
          }
      });
      

      远远没有最大的CPU使用率(这比仅仅最大化其时钟,有多个缓存级别,多个管道等等更复杂)但它是可读的,快速的和平台无关的(除了整数大小,但是C#int是System.Int32的别名所以我们在这里很安全。)

      在这里,我认为我们将停止优化。 它是作为一篇文章而不是回答而出现的,我希望没有人会为此清除我。

答案 2 :(得分:3)

这是另一种(不太可读但可能更高效)的方法,它不需要LINQ:

public static int[] GetDifference(int[] first, int[] second)
{
    int commonLength = Math.Min(first.Length, second.Length);
    int[] diff = new int[commonLength];
    for (int i = 0; i < commonLength; i++)
        diff[i] = Math.Abs(first[i] - second[i]);
    return diff;
}

为什么效率更高?因为ToArray has to resize the array直到它知道最终尺寸。

答案 3 :(得分:1)

var data3 = data1.Select((x,i)=>new {x,i})
    .Join
    (
        data2.Select((x,i)=>new {x,i}),
        x=>x.i,
        x=>x.i,
        (d1,d2)=>Math.Abs(d1.x-d2.x)
    )
    .ToArray();