比较 2 字节数组

Posted

技术标签:

【中文标题】比较 2 字节数组【英文标题】:Compare 2 byte arrays 【发布时间】:2014-11-02 19:25:45 【问题描述】:

我有 2 个 int 数组。

int[] data1 #
int[] data2 #

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

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

值为 15(例如)。

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

值为 3(例如)。

data3 中的第一个值是 12。

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

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

那么差值为-12。但我希望它只有 12 岁。

目前我有一个 for 循环,我在那里进行计算以获得那种类型的结果。

    有没有办法做到 data1-data2 = data3 不通过枚举 一个循环? 如果是这样,我可以在不使用减号的情况下获得差异吗 数字?

谢谢

注意 回应“关闭者”。我在一定程度上同意你的看法。我需要添加到这个问题的是:

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

【问题讨论】:

我稍微升级了我的答案。性能提升可能对您的数据大小有用。 Aaand 又一个性能提升的更新 ;) @ptwr 你花了你的时间:)。说真的,非常感谢。会试一试。谢谢 如果你以为我已经完成了,那你就错了!虽然这次我认为它的最终版本。 @PTwr 嗨,你是个痴迷者!但我没有抱怨。下班后我会看看这个 - 谢谢! 【参考方案1】:

你正在寻找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),它简单地减去两个数字并得到绝对值结果。然后返回一个序列,其中包含每个操作的结果。

【讨论】:

嗨,有趣。 Zip 似乎不是 data1 的属性。显然,我在这里遗漏了什么? @AndrewSimpson using System.LINQ? 啊!,usie LinQ - 对不起 嗨,我现在正在尝试你和@GiannisParaskevopoulos,因为效率是关键。 刚试过这个。好东西。请问 Zip 在这种情况下实际上做了什么?【参考方案2】:

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

最好,或者更糟的是,您可以调用将为您进行枚举的函数。但它会很慢。在 LINQ 的情况下,速度太慢了。

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

23560 滴答声 - Giannis Paraskevopoulos。 Array-Enumerable-Array 转换不是很快,通过 ToList().ToArray() 链复制数组比 Array.Copy() 慢大约 25 倍。 10198 个滴答声 - Selman22。快了 2 倍,但仍然很慢。 Lambda 让创建事件更漂亮,而不是更快。你最终会得到一些匿名方法,这可能比它的操作花费更多的 CPU 时间来返回调用(请记住,我们在这里做的数学运算 CPU 可以在几个周期内完成)。 566 个滴答声 - 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。

    Unroll loop。或者不要。您的体验可能会有所不同。即使在 汇编程序本身循环展开性能增益或损失变化 在同一系列处理器中非常重要。新的 CPU 和编译器是 了解旧技巧并简单地自行实施它们。为了 i3-3220 我在循环展开到 4 行时测试代码导致更快 在 32 位代码上执行,但在 64 位上它有点慢,而展开到 8 则相反。 为 x64 编译。因为我们在这里处理 32 位数据,所以我们不会制作 使用 64 位寄存器……还是我们会?在 x86 上不到一半 寄存器真正可用于生成的代码(在汇编中 手动您总是可以挤出更多),在 x64 上,您可以获得八个免费使用的奖励寄存器。您可以在不访问内存的情况下做的越多,您的代码就越快。在这种情况下,速度增益约为 20%。 关闭 Visual Studio。不要在 32 位 IDE 中对 64 位代码进行速度测试 (目前还没有 64 位版本,probably wont be for long time)。它会使 x64 代码大约慢两倍,因为 架构不匹配。 (嗯......无论如何,你永远不应该在调试器下对代码进行速度测试......) 不要过多使用内置函数。在这种情况下 Math.Abs​​ 有 overhead hidden inside。由于某些原因(需要分析 IL 来找出),使用 ?: 检查负值比使用 If-Else 更快。这样的检查节省了很多时间。

更新: ?: 比 If-Else 更快,因为生成的机器代码存在差异……至少对于比较两个值而言。它的机器代码远没有 If-Else 奇怪(它看起来不像你会“手工”编写的代码)。显然,这不仅仅是编写 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。 XOR 有什么了不起的?除了通常的行为,您还可以将其转换为 NOT(否定)和 NOP(无操作)。

1 XOR 1 = 0
0 XOR 1 = 1

所以按仅填充 1 的值进行异或运算会得到 NOT 运算。

1 XOR 0 = 1
0 XOR 0 = 0

所以按只填充 0 的值进行异或运算根本没有任何作用。

我们可以从我们的号码中获得符号。对于 32 位整数,它就像 x&gt;&gt;31 一样简单。它将位符号移动到最低位。甚至 wiki 都会告诉您,从左侧插入的位将为零,因此您的 x&gt;&gt;31 的结果将为负数 (x=0) 为 0,对吧?

不。对于有符号值,Arithmetic shift 用于普通位移。所以我们将根据符号得到 -1 或 0.... 这意味着 'x>>31' 将给出 111...111 为负数, 000...000 为非负数。如果您将通过这种移位的结果对原始 x 进行异或运算,您将根据值符号执行 NOT 或 NOP。另一个有用的事情是,0 将导致 NOP 进行加法/否定,因此我们可以根据值符号加/减 -1。

所以 'x^(x>>31)' 将翻转负数位,同时不对非负数进行任何更改,而 'x-(x>>31)' 将加 1(负数为正数)负 x 且不更改非负值。

结合起来你会得到'(x ^ (x >> 31)) - (x >> 31)'... 可以翻译成:

IF X<0
  X=!X+1

只是

IF X<0
  X=-X

它如何影响性能? 我们的 XorAbs() 只需要四次基本整数运算,一次加载一次存储。分支运算符本​​身占用的 CPU 滴答数差不多。虽然现代 CPU 擅长进行分支预测,但它们仍然更快,因为在提供顺序代码时根本不这样做。

分数是多少?

    比内置 Abs() 快大约四倍; 大约是以前代码的两倍(没有展开的版本) 根据 CPU 的不同,它可以在不展开循环的情况下获得更好的结果。 由于消除了代码分支,CPU 可以在其上“展开”循环 自己的。 (Haswells 在展开时很奇怪)

结果代码:

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.ForEach 和 Load 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 的别名,所以我们在这里是安全的)。

我想我们将停止优化。 它是作为一篇文章而不是答案出来的,我希望没有人会因此而清洗我。

【讨论】:

您好,感谢您提供的所有信息。是的,我曾认为使用 LINQ 比“传统”方法慢。这和蒂姆的代码一样吗?但是你给了我更多的信息 @AndrewSimpson 是的,我使用了他们的代码。此外,我刚刚更新了基于 LINQ 代码的更好结果的答案。 只是确保我没有遗漏任何东西。您提供了速度结果的额外信息,这给了我上下文。谢谢 @PTwr:为什么我的GetDifference 需要比你的循环多 20 倍?我也只是使用一个循环,所以完全相同的代码。另外,你是如何测试它的?我还会删除以 “Such basic memory operations” 开头的句子,因为它非常令人困惑,而且 LINQ 的效率通常不会明显降低。 @PTwr:非方法和方法is absolutely negligible的区别。编写安全、可读和可维护的代码,而不是在一百万次迭代中减少几毫秒的代码。【参考方案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;

为什么效率更高?因为ToArrayhas to resize the array直到它知道最终的大小。

【讨论】:

天哪!我在这里被宠坏了!每个人都有很棒的东西。我需要加快测试这 3 种方法,我会尽快报告。谢谢 @AndrewSimpson:数组是大还是小,但您需要经常调用它? @AndrewSimpson:除非 GetDifference 是代码中的瓶颈,否则更喜欢可读性,而不是性能。 嗨,数组大小是展平二维数组的结果。初始二维数组为 144x180。因此,得到的扁平数组是 25920。我正在尽可能多地比较这些数组(这是我的 FPS)速率。所以,每秒很多次.. @Brian:我也喜欢 LINQ 并且支持 Zip 方法。但是由于 OP 已经提到性能很重要,所以我提供了一种非 LINQ 方法。此外,这种方法并不复杂。【参考方案4】:
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();

【讨论】:

以上是关于比较 2 字节数组的主要内容,如果未能解决你的问题,请参考以下文章

比较两个字节数组的最快方法是啥?

在 F# 中比较两个字节数组的最快方法

比较 .NET 中的两个字节数组

Java中单字节Ascii的byte字节数组与String转换

GO中常用包笔记 bytes

GO中常用包笔记 bytes