Add Hardware Accelerated Checksums - #1201

Merged
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash
May 18, 2020
Merged

Add Hardware Accelerated Checksums#1201
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash

Conversation

@JimBobSquarePants

@JimBobSquarePantsJimBobSquarePants commented May 16, 2020

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Added support for hardware accelerated Crc32 and Adler32 checksum generation both used by our png codecs. They're fast!

Benchmarks.

Adler32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.21024793.18 ns775.66 ns42.516 ns1.000.00----
SixLaborsCalculate.NET 4.7.21024384.86 ns15.64 ns0.857 ns0.490.03----
SharpZipLibCalculate.NET Core 2.11024790.31 ns353.34 ns19.368 ns1.000.00----
SixLaborsCalculate.NET Core 2.11024465.28 ns652.41 ns35.761 ns0.590.03----
SharpZipLibCalculate.NET Core 3.11024877.25 ns97.89 ns5.365 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102445.60 ns13.28 ns0.728 ns0.050.00----
SharpZipLibCalculate.NET 4.7.220481,537.04 ns428.44 ns23.484 ns1.000.00----
SixLaborsCalculate.NET 4.7.22048849.76 ns1,066.34 ns58.450 ns0.550.04----
SharpZipLibCalculate.NET Core 2.120481,616.97 ns276.70 ns15.167 ns1.000.00----
SixLaborsCalculate.NET Core 2.12048790.77 ns691.71 ns37.915 ns0.490.03----
SharpZipLibCalculate.NET Core 3.120481,735.11 ns1,374.22 ns75.325 ns1.000.00----
SixLaborsCalculate.NET Core 3.1204887.80 ns56.84 ns3.116 ns0.050.00----
SharpZipLibCalculate.NET 4.7.240963,054.53 ns796.41 ns43.654 ns1.000.00----
SixLaborsCalculate.NET 4.7.240961,538.90 ns487.02 ns26.695 ns0.500.01----
SharpZipLibCalculate.NET Core 2.140963,223.48 ns32.32 ns1.771 ns1.000.00----
SixLaborsCalculate.NET Core 2.140961,547.60 ns309.72 ns16.977 ns0.480.01----
SharpZipLibCalculate.NET Core 3.140963,672.33 ns1,095.81 ns60.065 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096159.44 ns36.31 ns1.990 ns0.040.00----

Crc32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.210243,067.24 ns769.25 ns42.165 ns1.000.00----
SixLaborsCalculate.NET 4.7.210242,546.86 ns1,106.36 ns60.643 ns0.830.02----
SharpZipLibCalculate.NET Core 2.110243,377.15 ns3,903.41 ns213.959 ns1.000.00----
SixLaborsCalculate.NET Core 2.110242,524.25 ns2,220.97 ns121.739 ns0.750.04----
SharpZipLibCalculate.NET Core 3.110243,980.60 ns8,497.37 ns465.769 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102478.68 ns69.82 ns3.827 ns0.020.00----
SharpZipLibCalculate.NET 4.7.220487,934.29 ns42,550.13 ns2,332.316 ns1.000.00----
SixLaborsCalculate.NET 4.7.220485,437.81 ns12,760.51 ns699.447 ns0.710.10----
SharpZipLibCalculate.NET Core 2.120486,008.05 ns621.37 ns34.059 ns1.000.00----
SixLaborsCalculate.NET Core 2.120484,791.50 ns3,894.94 ns213.495 ns0.800.04----
SharpZipLibCalculate.NET Core 3.120485,900.06 ns1,344.70 ns73.707 ns1.000.00----
SixLaborsCalculate.NET Core 3.12048103.12 ns15.66 ns0.859 ns0.020.00----
SharpZipLibCalculate.NET 4.7.2409612,422.59 ns1,308.01 ns71.696 ns1.000.00----
SixLaborsCalculate.NET 4.7.2409610,524.63 ns6,267.56 ns343.546 ns0.850.03----
SharpZipLibCalculate.NET Core 2.1409611,888.00 ns1,059.25 ns58.061 ns1.000.00----
SixLaborsCalculate.NET Core 2.140969,806.24 ns241.91 ns13.260 ns0.820.00----
SharpZipLibCalculate.NET Core 3.1409612,181.28 ns1,974.68 ns108.239 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096192.39 ns10.27 ns0.563 ns0.020.00----

@saucecontrolsaucecontrol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool to see this happening! I left a few suggestions for improvements if you want to push it further.

const byte S2301 = 0b1011_0001; // A B C D -> B A D C
const byte S1032 = 0b0100_1110; // A B C D -> C D A B

v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

This was a mistake in the Chromium code. The odd elements of the s1 vector are always 0, so this shuffle/add pair doesn't do anything.

Comment on lines +109 to +111
Vector128<int> v_ps = Vector128.CreateScalar(s1 * n).AsInt32();
Vector128<int> v_s2 = Vector128.CreateScalar(s2).AsInt32();
Vector128<int> v_s1 = Vector128<int>.Zero;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Vector128<int>v_ps=Vector128.CreateScalar(s1*n).AsInt32();
Vector128<int>v_s2=Vector128.CreateScalar(s2).AsInt32();
Vector128<int>v_s1=Vector128<int>.Zero;
Vector128<uint>v_ps=Vector128.CreateScalar(s1*n);
Vector128<uint>v_s2=Vector128.CreateScalar(s2);
Vector128<uint>v_s1=Vector128<uint>.Zero;

The logic depends on these values not overflowing uint.MaxValue when processing NMAX bytes. Best to keep them unsigned all the way through.

Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsUInt32());


// Horizontally add the bytes for s1, multiply-adds the
// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsUInt32());

v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S2301));
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S1032));

s2 = (uint)v_s2.ToScalar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s2=(uint)v_s2.ToScalar();
s2=v_s2.ToScalar();

// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones));
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones).AsUInt32());

Comment on lines +92 to +93
var tap1 = Vector128.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17);
var tap2 = Vector128.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vector128.Create() with a lot of elements is quite inefficient on netcoreapp3.x. @tannergooding recently fixed it up for net5.0 in dotnet/runtime#35857, but you might want to use the ROS trick to load these from fixed data since 3.1 will be around for a while.

Comment on lines +162 to +167
if (length >= 16)
{
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an odd choice by the Chromium dev(s). Since NMAX is an even multiple of 16 but not of 32, you could always safely process a 16-byte straggler as a half iteration of the SIMD loop. No need for this big manually-unrolled block.

For that matter, if the typical data length is greater than, say, 128 bytes the algorithm would extend naturally to AVX2, which would allow 64 bytes per iteration using basically the same code.

Comment on lines +234 to +237
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JIT doesn't do great with code like this.

On legacy jit32, it emits this:

 L0031: movzxeax, byte [ebx+eax] L0035: addedx,eax L0037: addecx,edx L0039: moveax,esi L003b: incesi L003c: movzxeax, byte [ebx+eax] L0040: addedx,eax L0042: addecx,edx L0044: moveax,esi L0046: incesi

RyuJIT does a bit better by not actually incrementing index each time, but it emits an extra mov for each block because it's confused by your two variables:

 L0031: movzxr9d, byte ptr [rax+1] L0036: addr9d,ecx L0039: movecx,r9d L003c: addr8d,ecx L003f: movzxr9d, byte ptr [rax+2] L0044: addr9d,ecx L0047: movecx,r9d L004a: addr8d,ecx

Writing it more like the C code:

byte*pbuff=pbuffer+index;if(length>=16){s2+=(s1+=pbuff[0]);s2+=(s1+=pbuff[1]);s2+=(s1+=pbuff[2]); ...}

gives better codegen in both:

 L0042: movzxr9d, byte ptr [rcx+1] L0047: addeax,r9d L004a: addr8d,eax L004d: movzxr9d, byte ptr [rcx+2] L0052: addeax,r9d L0055: addr8d,eax

SharpLab for both variants

It's also not clear that unroll by 16 is ideal for modern processors. This may be an outdated optimization from zlib. Might be worth testing unroll by 8 or even 4 to see how they do if you haven't already.

Comment on lines +83 to +86
fixed (ulong* k1k2Ptr = &k1k2[0])
fixed (ulong* k3k4Ptr = &k3k4[0])
fixed (ulong* k5k0Ptr = &k5k0[0])
fixed (ulong* polyPtr = &poly[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another place where the ROS trick would be of benefit. Instead of 4 separate unmanaged pointers, you could have a single pointer to a block containing all 4 vector values.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code to use a static readonly array as I wasn't sure the ROS optimizations apply for anything other than byte?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the compiler won't optimize anything bigger than byte because of endianness concerns. Since this code is processor-specific, you can outsmart the compiler by breaking the ulongs up into bytes (and reversing their byte order, ofc)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that for now since the massive hashing improvements have actually made less of an impact than I'd hoped. I'm going to profile now and find other soft targets.

@antonfirsovantonfirsov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (without going into implementation details).


// act
crc.Update(data);
// Longer run, enough to require moving the point in SIMD implementation with

@antonfirsovantonfirsovMay 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there there are some other corner cases worth of testing?
Eg: data.Length == 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, The current test isn't great, I'll add better reference tests

@JimBobSquarePants

JimBobSquarePants commented May 18, 2020

Copy link
Copy Markdown
MemberAuthor

@saucecontrol Thanks for the review! Some great improvement ideas there. 👍 I've committed changes in one go rather than accepting individual suggestions as I didn't want to keep triggering the build systems.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@JimBobSquarePants@antonfirsov@saucecontrol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add Hardware Accelerated Checksums - #1201

Merged
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash
May 18, 2020
Merged

Add Hardware Accelerated Checksums#1201
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash

Conversation

@JimBobSquarePants

@JimBobSquarePantsJimBobSquarePants commented May 16, 2020

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Added support for hardware accelerated Crc32 and Adler32 checksum generation both used by our png codecs. They're fast!

Benchmarks.

Adler32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.21024793.18 ns775.66 ns42.516 ns1.000.00----
SixLaborsCalculate.NET 4.7.21024384.86 ns15.64 ns0.857 ns0.490.03----
SharpZipLibCalculate.NET Core 2.11024790.31 ns353.34 ns19.368 ns1.000.00----
SixLaborsCalculate.NET Core 2.11024465.28 ns652.41 ns35.761 ns0.590.03----
SharpZipLibCalculate.NET Core 3.11024877.25 ns97.89 ns5.365 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102445.60 ns13.28 ns0.728 ns0.050.00----
SharpZipLibCalculate.NET 4.7.220481,537.04 ns428.44 ns23.484 ns1.000.00----
SixLaborsCalculate.NET 4.7.22048849.76 ns1,066.34 ns58.450 ns0.550.04----
SharpZipLibCalculate.NET Core 2.120481,616.97 ns276.70 ns15.167 ns1.000.00----
SixLaborsCalculate.NET Core 2.12048790.77 ns691.71 ns37.915 ns0.490.03----
SharpZipLibCalculate.NET Core 3.120481,735.11 ns1,374.22 ns75.325 ns1.000.00----
SixLaborsCalculate.NET Core 3.1204887.80 ns56.84 ns3.116 ns0.050.00----
SharpZipLibCalculate.NET 4.7.240963,054.53 ns796.41 ns43.654 ns1.000.00----
SixLaborsCalculate.NET 4.7.240961,538.90 ns487.02 ns26.695 ns0.500.01----
SharpZipLibCalculate.NET Core 2.140963,223.48 ns32.32 ns1.771 ns1.000.00----
SixLaborsCalculate.NET Core 2.140961,547.60 ns309.72 ns16.977 ns0.480.01----
SharpZipLibCalculate.NET Core 3.140963,672.33 ns1,095.81 ns60.065 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096159.44 ns36.31 ns1.990 ns0.040.00----

Crc32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.210243,067.24 ns769.25 ns42.165 ns1.000.00----
SixLaborsCalculate.NET 4.7.210242,546.86 ns1,106.36 ns60.643 ns0.830.02----
SharpZipLibCalculate.NET Core 2.110243,377.15 ns3,903.41 ns213.959 ns1.000.00----
SixLaborsCalculate.NET Core 2.110242,524.25 ns2,220.97 ns121.739 ns0.750.04----
SharpZipLibCalculate.NET Core 3.110243,980.60 ns8,497.37 ns465.769 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102478.68 ns69.82 ns3.827 ns0.020.00----
SharpZipLibCalculate.NET 4.7.220487,934.29 ns42,550.13 ns2,332.316 ns1.000.00----
SixLaborsCalculate.NET 4.7.220485,437.81 ns12,760.51 ns699.447 ns0.710.10----
SharpZipLibCalculate.NET Core 2.120486,008.05 ns621.37 ns34.059 ns1.000.00----
SixLaborsCalculate.NET Core 2.120484,791.50 ns3,894.94 ns213.495 ns0.800.04----
SharpZipLibCalculate.NET Core 3.120485,900.06 ns1,344.70 ns73.707 ns1.000.00----
SixLaborsCalculate.NET Core 3.12048103.12 ns15.66 ns0.859 ns0.020.00----
SharpZipLibCalculate.NET 4.7.2409612,422.59 ns1,308.01 ns71.696 ns1.000.00----
SixLaborsCalculate.NET 4.7.2409610,524.63 ns6,267.56 ns343.546 ns0.850.03----
SharpZipLibCalculate.NET Core 2.1409611,888.00 ns1,059.25 ns58.061 ns1.000.00----
SixLaborsCalculate.NET Core 2.140969,806.24 ns241.91 ns13.260 ns0.820.00----
SharpZipLibCalculate.NET Core 3.1409612,181.28 ns1,974.68 ns108.239 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096192.39 ns10.27 ns0.563 ns0.020.00----

@saucecontrolsaucecontrol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool to see this happening! I left a few suggestions for improvements if you want to push it further.

const byte S2301 = 0b1011_0001; // A B C D -> B A D C
const byte S1032 = 0b0100_1110; // A B C D -> C D A B

v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

This was a mistake in the Chromium code. The odd elements of the s1 vector are always 0, so this shuffle/add pair doesn't do anything.

Comment on lines +109 to +111
Vector128<int> v_ps = Vector128.CreateScalar(s1 * n).AsInt32();
Vector128<int> v_s2 = Vector128.CreateScalar(s2).AsInt32();
Vector128<int> v_s1 = Vector128<int>.Zero;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Vector128<int>v_ps=Vector128.CreateScalar(s1*n).AsInt32();
Vector128<int>v_s2=Vector128.CreateScalar(s2).AsInt32();
Vector128<int>v_s1=Vector128<int>.Zero;
Vector128<uint>v_ps=Vector128.CreateScalar(s1*n);
Vector128<uint>v_s2=Vector128.CreateScalar(s2);
Vector128<uint>v_s1=Vector128<uint>.Zero;

The logic depends on these values not overflowing uint.MaxValue when processing NMAX bytes. Best to keep them unsigned all the way through.

Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsUInt32());


// Horizontally add the bytes for s1, multiply-adds the
// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsUInt32());

v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S2301));
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S1032));

s2 = (uint)v_s2.ToScalar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s2=(uint)v_s2.ToScalar();
s2=v_s2.ToScalar();

// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones));
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones).AsUInt32());

Comment on lines +92 to +93
var tap1 = Vector128.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17);
var tap2 = Vector128.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vector128.Create() with a lot of elements is quite inefficient on netcoreapp3.x. @tannergooding recently fixed it up for net5.0 in dotnet/runtime#35857, but you might want to use the ROS trick to load these from fixed data since 3.1 will be around for a while.

Comment on lines +162 to +167
if (length >= 16)
{
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an odd choice by the Chromium dev(s). Since NMAX is an even multiple of 16 but not of 32, you could always safely process a 16-byte straggler as a half iteration of the SIMD loop. No need for this big manually-unrolled block.

For that matter, if the typical data length is greater than, say, 128 bytes the algorithm would extend naturally to AVX2, which would allow 64 bytes per iteration using basically the same code.

Comment on lines +234 to +237
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JIT doesn't do great with code like this.

On legacy jit32, it emits this:

 L0031: movzxeax, byte [ebx+eax] L0035: addedx,eax L0037: addecx,edx L0039: moveax,esi L003b: incesi L003c: movzxeax, byte [ebx+eax] L0040: addedx,eax L0042: addecx,edx L0044: moveax,esi L0046: incesi

RyuJIT does a bit better by not actually incrementing index each time, but it emits an extra mov for each block because it's confused by your two variables:

 L0031: movzxr9d, byte ptr [rax+1] L0036: addr9d,ecx L0039: movecx,r9d L003c: addr8d,ecx L003f: movzxr9d, byte ptr [rax+2] L0044: addr9d,ecx L0047: movecx,r9d L004a: addr8d,ecx

Writing it more like the C code:

byte*pbuff=pbuffer+index;if(length>=16){s2+=(s1+=pbuff[0]);s2+=(s1+=pbuff[1]);s2+=(s1+=pbuff[2]); ...}

gives better codegen in both:

 L0042: movzxr9d, byte ptr [rcx+1] L0047: addeax,r9d L004a: addr8d,eax L004d: movzxr9d, byte ptr [rcx+2] L0052: addeax,r9d L0055: addr8d,eax

SharpLab for both variants

It's also not clear that unroll by 16 is ideal for modern processors. This may be an outdated optimization from zlib. Might be worth testing unroll by 8 or even 4 to see how they do if you haven't already.

Comment on lines +83 to +86
fixed (ulong* k1k2Ptr = &k1k2[0])
fixed (ulong* k3k4Ptr = &k3k4[0])
fixed (ulong* k5k0Ptr = &k5k0[0])
fixed (ulong* polyPtr = &poly[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another place where the ROS trick would be of benefit. Instead of 4 separate unmanaged pointers, you could have a single pointer to a block containing all 4 vector values.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code to use a static readonly array as I wasn't sure the ROS optimizations apply for anything other than byte?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the compiler won't optimize anything bigger than byte because of endianness concerns. Since this code is processor-specific, you can outsmart the compiler by breaking the ulongs up into bytes (and reversing their byte order, ofc)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that for now since the massive hashing improvements have actually made less of an impact than I'd hoped. I'm going to profile now and find other soft targets.

@antonfirsovantonfirsov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (without going into implementation details).


// act
crc.Update(data);
// Longer run, enough to require moving the point in SIMD implementation with

@antonfirsovantonfirsovMay 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there there are some other corner cases worth of testing?
Eg: data.Length == 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, The current test isn't great, I'll add better reference tests

@JimBobSquarePants

JimBobSquarePants commented May 18, 2020

Copy link
Copy Markdown
MemberAuthor

@saucecontrol Thanks for the review! Some great improvement ideas there. 👍 I've committed changes in one go rather than accepting individual suggestions as I didn't want to keep triggering the build systems.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@JimBobSquarePants@antonfirsov@saucecontrol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Hardware Accelerated Checksums - #1201

Merged
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash
May 18, 2020
Merged

Add Hardware Accelerated Checksums#1201
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash

Conversation

@JimBobSquarePants

@JimBobSquarePantsJimBobSquarePants commented May 16, 2020

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Added support for hardware accelerated Crc32 and Adler32 checksum generation both used by our png codecs. They're fast!

Benchmarks.

Adler32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.21024793.18 ns775.66 ns42.516 ns1.000.00----
SixLaborsCalculate.NET 4.7.21024384.86 ns15.64 ns0.857 ns0.490.03----
SharpZipLibCalculate.NET Core 2.11024790.31 ns353.34 ns19.368 ns1.000.00----
SixLaborsCalculate.NET Core 2.11024465.28 ns652.41 ns35.761 ns0.590.03----
SharpZipLibCalculate.NET Core 3.11024877.25 ns97.89 ns5.365 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102445.60 ns13.28 ns0.728 ns0.050.00----
SharpZipLibCalculate.NET 4.7.220481,537.04 ns428.44 ns23.484 ns1.000.00----
SixLaborsCalculate.NET 4.7.22048849.76 ns1,066.34 ns58.450 ns0.550.04----
SharpZipLibCalculate.NET Core 2.120481,616.97 ns276.70 ns15.167 ns1.000.00----
SixLaborsCalculate.NET Core 2.12048790.77 ns691.71 ns37.915 ns0.490.03----
SharpZipLibCalculate.NET Core 3.120481,735.11 ns1,374.22 ns75.325 ns1.000.00----
SixLaborsCalculate.NET Core 3.1204887.80 ns56.84 ns3.116 ns0.050.00----
SharpZipLibCalculate.NET 4.7.240963,054.53 ns796.41 ns43.654 ns1.000.00----
SixLaborsCalculate.NET 4.7.240961,538.90 ns487.02 ns26.695 ns0.500.01----
SharpZipLibCalculate.NET Core 2.140963,223.48 ns32.32 ns1.771 ns1.000.00----
SixLaborsCalculate.NET Core 2.140961,547.60 ns309.72 ns16.977 ns0.480.01----
SharpZipLibCalculate.NET Core 3.140963,672.33 ns1,095.81 ns60.065 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096159.44 ns36.31 ns1.990 ns0.040.00----

Crc32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.210243,067.24 ns769.25 ns42.165 ns1.000.00----
SixLaborsCalculate.NET 4.7.210242,546.86 ns1,106.36 ns60.643 ns0.830.02----
SharpZipLibCalculate.NET Core 2.110243,377.15 ns3,903.41 ns213.959 ns1.000.00----
SixLaborsCalculate.NET Core 2.110242,524.25 ns2,220.97 ns121.739 ns0.750.04----
SharpZipLibCalculate.NET Core 3.110243,980.60 ns8,497.37 ns465.769 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102478.68 ns69.82 ns3.827 ns0.020.00----
SharpZipLibCalculate.NET 4.7.220487,934.29 ns42,550.13 ns2,332.316 ns1.000.00----
SixLaborsCalculate.NET 4.7.220485,437.81 ns12,760.51 ns699.447 ns0.710.10----
SharpZipLibCalculate.NET Core 2.120486,008.05 ns621.37 ns34.059 ns1.000.00----
SixLaborsCalculate.NET Core 2.120484,791.50 ns3,894.94 ns213.495 ns0.800.04----
SharpZipLibCalculate.NET Core 3.120485,900.06 ns1,344.70 ns73.707 ns1.000.00----
SixLaborsCalculate.NET Core 3.12048103.12 ns15.66 ns0.859 ns0.020.00----
SharpZipLibCalculate.NET 4.7.2409612,422.59 ns1,308.01 ns71.696 ns1.000.00----
SixLaborsCalculate.NET 4.7.2409610,524.63 ns6,267.56 ns343.546 ns0.850.03----
SharpZipLibCalculate.NET Core 2.1409611,888.00 ns1,059.25 ns58.061 ns1.000.00----
SixLaborsCalculate.NET Core 2.140969,806.24 ns241.91 ns13.260 ns0.820.00----
SharpZipLibCalculate.NET Core 3.1409612,181.28 ns1,974.68 ns108.239 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096192.39 ns10.27 ns0.563 ns0.020.00----

@saucecontrolsaucecontrol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool to see this happening! I left a few suggestions for improvements if you want to push it further.

const byte S2301 = 0b1011_0001; // A B C D -> B A D C
const byte S1032 = 0b0100_1110; // A B C D -> C D A B

v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

This was a mistake in the Chromium code. The odd elements of the s1 vector are always 0, so this shuffle/add pair doesn't do anything.

Comment on lines +109 to +111
Vector128<int> v_ps = Vector128.CreateScalar(s1 * n).AsInt32();
Vector128<int> v_s2 = Vector128.CreateScalar(s2).AsInt32();
Vector128<int> v_s1 = Vector128<int>.Zero;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Vector128<int>v_ps=Vector128.CreateScalar(s1*n).AsInt32();
Vector128<int>v_s2=Vector128.CreateScalar(s2).AsInt32();
Vector128<int>v_s1=Vector128<int>.Zero;
Vector128<uint>v_ps=Vector128.CreateScalar(s1*n);
Vector128<uint>v_s2=Vector128.CreateScalar(s2);
Vector128<uint>v_s1=Vector128<uint>.Zero;

The logic depends on these values not overflowing uint.MaxValue when processing NMAX bytes. Best to keep them unsigned all the way through.

Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsUInt32());


// Horizontally add the bytes for s1, multiply-adds the
// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsUInt32());

v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S2301));
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S1032));

s2 = (uint)v_s2.ToScalar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s2=(uint)v_s2.ToScalar();
s2=v_s2.ToScalar();

// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones));
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones).AsUInt32());

Comment on lines +92 to +93
var tap1 = Vector128.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17);
var tap2 = Vector128.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vector128.Create() with a lot of elements is quite inefficient on netcoreapp3.x. @tannergooding recently fixed it up for net5.0 in dotnet/runtime#35857, but you might want to use the ROS trick to load these from fixed data since 3.1 will be around for a while.

Comment on lines +162 to +167
if (length >= 16)
{
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an odd choice by the Chromium dev(s). Since NMAX is an even multiple of 16 but not of 32, you could always safely process a 16-byte straggler as a half iteration of the SIMD loop. No need for this big manually-unrolled block.

For that matter, if the typical data length is greater than, say, 128 bytes the algorithm would extend naturally to AVX2, which would allow 64 bytes per iteration using basically the same code.

Comment on lines +234 to +237
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JIT doesn't do great with code like this.

On legacy jit32, it emits this:

 L0031: movzxeax, byte [ebx+eax] L0035: addedx,eax L0037: addecx,edx L0039: moveax,esi L003b: incesi L003c: movzxeax, byte [ebx+eax] L0040: addedx,eax L0042: addecx,edx L0044: moveax,esi L0046: incesi

RyuJIT does a bit better by not actually incrementing index each time, but it emits an extra mov for each block because it's confused by your two variables:

 L0031: movzxr9d, byte ptr [rax+1] L0036: addr9d,ecx L0039: movecx,r9d L003c: addr8d,ecx L003f: movzxr9d, byte ptr [rax+2] L0044: addr9d,ecx L0047: movecx,r9d L004a: addr8d,ecx

Writing it more like the C code:

byte*pbuff=pbuffer+index;if(length>=16){s2+=(s1+=pbuff[0]);s2+=(s1+=pbuff[1]);s2+=(s1+=pbuff[2]); ...}

gives better codegen in both:

 L0042: movzxr9d, byte ptr [rcx+1] L0047: addeax,r9d L004a: addr8d,eax L004d: movzxr9d, byte ptr [rcx+2] L0052: addeax,r9d L0055: addr8d,eax

SharpLab for both variants

It's also not clear that unroll by 16 is ideal for modern processors. This may be an outdated optimization from zlib. Might be worth testing unroll by 8 or even 4 to see how they do if you haven't already.

Comment on lines +83 to +86
fixed (ulong* k1k2Ptr = &k1k2[0])
fixed (ulong* k3k4Ptr = &k3k4[0])
fixed (ulong* k5k0Ptr = &k5k0[0])
fixed (ulong* polyPtr = &poly[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another place where the ROS trick would be of benefit. Instead of 4 separate unmanaged pointers, you could have a single pointer to a block containing all 4 vector values.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code to use a static readonly array as I wasn't sure the ROS optimizations apply for anything other than byte?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the compiler won't optimize anything bigger than byte because of endianness concerns. Since this code is processor-specific, you can outsmart the compiler by breaking the ulongs up into bytes (and reversing their byte order, ofc)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that for now since the massive hashing improvements have actually made less of an impact than I'd hoped. I'm going to profile now and find other soft targets.

@antonfirsovantonfirsov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (without going into implementation details).


// act
crc.Update(data);
// Longer run, enough to require moving the point in SIMD implementation with

@antonfirsovantonfirsovMay 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there there are some other corner cases worth of testing?
Eg: data.Length == 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, The current test isn't great, I'll add better reference tests

@JimBobSquarePants

JimBobSquarePants commented May 18, 2020

Copy link
Copy Markdown
MemberAuthor

@saucecontrol Thanks for the review! Some great improvement ideas there. 👍 I've committed changes in one go rather than accepting individual suggestions as I didn't want to keep triggering the build systems.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@JimBobSquarePants@antonfirsov@saucecontrol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Hardware Accelerated Checksums - #1201

Merged
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash
May 18, 2020
Merged

Add Hardware Accelerated Checksums#1201
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash

Conversation

@JimBobSquarePants

@JimBobSquarePantsJimBobSquarePants commented May 16, 2020

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Added support for hardware accelerated Crc32 and Adler32 checksum generation both used by our png codecs. They're fast!

Benchmarks.

Adler32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.21024793.18 ns775.66 ns42.516 ns1.000.00----
SixLaborsCalculate.NET 4.7.21024384.86 ns15.64 ns0.857 ns0.490.03----
SharpZipLibCalculate.NET Core 2.11024790.31 ns353.34 ns19.368 ns1.000.00----
SixLaborsCalculate.NET Core 2.11024465.28 ns652.41 ns35.761 ns0.590.03----
SharpZipLibCalculate.NET Core 3.11024877.25 ns97.89 ns5.365 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102445.60 ns13.28 ns0.728 ns0.050.00----
SharpZipLibCalculate.NET 4.7.220481,537.04 ns428.44 ns23.484 ns1.000.00----
SixLaborsCalculate.NET 4.7.22048849.76 ns1,066.34 ns58.450 ns0.550.04----
SharpZipLibCalculate.NET Core 2.120481,616.97 ns276.70 ns15.167 ns1.000.00----
SixLaborsCalculate.NET Core 2.12048790.77 ns691.71 ns37.915 ns0.490.03----
SharpZipLibCalculate.NET Core 3.120481,735.11 ns1,374.22 ns75.325 ns1.000.00----
SixLaborsCalculate.NET Core 3.1204887.80 ns56.84 ns3.116 ns0.050.00----
SharpZipLibCalculate.NET 4.7.240963,054.53 ns796.41 ns43.654 ns1.000.00----
SixLaborsCalculate.NET 4.7.240961,538.90 ns487.02 ns26.695 ns0.500.01----
SharpZipLibCalculate.NET Core 2.140963,223.48 ns32.32 ns1.771 ns1.000.00----
SixLaborsCalculate.NET Core 2.140961,547.60 ns309.72 ns16.977 ns0.480.01----
SharpZipLibCalculate.NET Core 3.140963,672.33 ns1,095.81 ns60.065 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096159.44 ns36.31 ns1.990 ns0.040.00----

Crc32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.210243,067.24 ns769.25 ns42.165 ns1.000.00----
SixLaborsCalculate.NET 4.7.210242,546.86 ns1,106.36 ns60.643 ns0.830.02----
SharpZipLibCalculate.NET Core 2.110243,377.15 ns3,903.41 ns213.959 ns1.000.00----
SixLaborsCalculate.NET Core 2.110242,524.25 ns2,220.97 ns121.739 ns0.750.04----
SharpZipLibCalculate.NET Core 3.110243,980.60 ns8,497.37 ns465.769 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102478.68 ns69.82 ns3.827 ns0.020.00----
SharpZipLibCalculate.NET 4.7.220487,934.29 ns42,550.13 ns2,332.316 ns1.000.00----
SixLaborsCalculate.NET 4.7.220485,437.81 ns12,760.51 ns699.447 ns0.710.10----
SharpZipLibCalculate.NET Core 2.120486,008.05 ns621.37 ns34.059 ns1.000.00----
SixLaborsCalculate.NET Core 2.120484,791.50 ns3,894.94 ns213.495 ns0.800.04----
SharpZipLibCalculate.NET Core 3.120485,900.06 ns1,344.70 ns73.707 ns1.000.00----
SixLaborsCalculate.NET Core 3.12048103.12 ns15.66 ns0.859 ns0.020.00----
SharpZipLibCalculate.NET 4.7.2409612,422.59 ns1,308.01 ns71.696 ns1.000.00----
SixLaborsCalculate.NET 4.7.2409610,524.63 ns6,267.56 ns343.546 ns0.850.03----
SharpZipLibCalculate.NET Core 2.1409611,888.00 ns1,059.25 ns58.061 ns1.000.00----
SixLaborsCalculate.NET Core 2.140969,806.24 ns241.91 ns13.260 ns0.820.00----
SharpZipLibCalculate.NET Core 3.1409612,181.28 ns1,974.68 ns108.239 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096192.39 ns10.27 ns0.563 ns0.020.00----

@saucecontrolsaucecontrol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool to see this happening! I left a few suggestions for improvements if you want to push it further.

const byte S2301 = 0b1011_0001; // A B C D -> B A D C
const byte S1032 = 0b0100_1110; // A B C D -> C D A B

v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

This was a mistake in the Chromium code. The odd elements of the s1 vector are always 0, so this shuffle/add pair doesn't do anything.

Comment on lines +109 to +111
Vector128<int> v_ps = Vector128.CreateScalar(s1 * n).AsInt32();
Vector128<int> v_s2 = Vector128.CreateScalar(s2).AsInt32();
Vector128<int> v_s1 = Vector128<int>.Zero;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Vector128<int>v_ps=Vector128.CreateScalar(s1*n).AsInt32();
Vector128<int>v_s2=Vector128.CreateScalar(s2).AsInt32();
Vector128<int>v_s1=Vector128<int>.Zero;
Vector128<uint>v_ps=Vector128.CreateScalar(s1*n);
Vector128<uint>v_s2=Vector128.CreateScalar(s2);
Vector128<uint>v_s1=Vector128<uint>.Zero;

The logic depends on these values not overflowing uint.MaxValue when processing NMAX bytes. Best to keep them unsigned all the way through.

Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsUInt32());


// Horizontally add the bytes for s1, multiply-adds the
// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsUInt32());

v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S2301));
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S1032));

s2 = (uint)v_s2.ToScalar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s2=(uint)v_s2.ToScalar();
s2=v_s2.ToScalar();

// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones));
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones).AsUInt32());

Comment on lines +92 to +93
var tap1 = Vector128.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17);
var tap2 = Vector128.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vector128.Create() with a lot of elements is quite inefficient on netcoreapp3.x. @tannergooding recently fixed it up for net5.0 in dotnet/runtime#35857, but you might want to use the ROS trick to load these from fixed data since 3.1 will be around for a while.

Comment on lines +162 to +167
if (length >= 16)
{
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an odd choice by the Chromium dev(s). Since NMAX is an even multiple of 16 but not of 32, you could always safely process a 16-byte straggler as a half iteration of the SIMD loop. No need for this big manually-unrolled block.

For that matter, if the typical data length is greater than, say, 128 bytes the algorithm would extend naturally to AVX2, which would allow 64 bytes per iteration using basically the same code.

Comment on lines +234 to +237
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JIT doesn't do great with code like this.

On legacy jit32, it emits this:

 L0031: movzxeax, byte [ebx+eax] L0035: addedx,eax L0037: addecx,edx L0039: moveax,esi L003b: incesi L003c: movzxeax, byte [ebx+eax] L0040: addedx,eax L0042: addecx,edx L0044: moveax,esi L0046: incesi

RyuJIT does a bit better by not actually incrementing index each time, but it emits an extra mov for each block because it's confused by your two variables:

 L0031: movzxr9d, byte ptr [rax+1] L0036: addr9d,ecx L0039: movecx,r9d L003c: addr8d,ecx L003f: movzxr9d, byte ptr [rax+2] L0044: addr9d,ecx L0047: movecx,r9d L004a: addr8d,ecx

Writing it more like the C code:

byte*pbuff=pbuffer+index;if(length>=16){s2+=(s1+=pbuff[0]);s2+=(s1+=pbuff[1]);s2+=(s1+=pbuff[2]); ...}

gives better codegen in both:

 L0042: movzxr9d, byte ptr [rcx+1] L0047: addeax,r9d L004a: addr8d,eax L004d: movzxr9d, byte ptr [rcx+2] L0052: addeax,r9d L0055: addr8d,eax

SharpLab for both variants

It's also not clear that unroll by 16 is ideal for modern processors. This may be an outdated optimization from zlib. Might be worth testing unroll by 8 or even 4 to see how they do if you haven't already.

Comment on lines +83 to +86
fixed (ulong* k1k2Ptr = &k1k2[0])
fixed (ulong* k3k4Ptr = &k3k4[0])
fixed (ulong* k5k0Ptr = &k5k0[0])
fixed (ulong* polyPtr = &poly[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another place where the ROS trick would be of benefit. Instead of 4 separate unmanaged pointers, you could have a single pointer to a block containing all 4 vector values.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code to use a static readonly array as I wasn't sure the ROS optimizations apply for anything other than byte?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the compiler won't optimize anything bigger than byte because of endianness concerns. Since this code is processor-specific, you can outsmart the compiler by breaking the ulongs up into bytes (and reversing their byte order, ofc)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that for now since the massive hashing improvements have actually made less of an impact than I'd hoped. I'm going to profile now and find other soft targets.

@antonfirsovantonfirsov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (without going into implementation details).


// act
crc.Update(data);
// Longer run, enough to require moving the point in SIMD implementation with

@antonfirsovantonfirsovMay 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there there are some other corner cases worth of testing?
Eg: data.Length == 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, The current test isn't great, I'll add better reference tests

@JimBobSquarePants

JimBobSquarePants commented May 18, 2020

Copy link
Copy Markdown
MemberAuthor

@saucecontrol Thanks for the review! Some great improvement ideas there. 👍 I've committed changes in one go rather than accepting individual suggestions as I didn't want to keep triggering the build systems.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@JimBobSquarePants@antonfirsov@saucecontrol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add Hardware Accelerated Checksums - #1201

Merged
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash
May 18, 2020
Merged

Add Hardware Accelerated Checksums#1201
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash

Conversation

@JimBobSquarePants

@JimBobSquarePantsJimBobSquarePants commented May 16, 2020

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Added support for hardware accelerated Crc32 and Adler32 checksum generation both used by our png codecs. They're fast!

Benchmarks.

Adler32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.21024793.18 ns775.66 ns42.516 ns1.000.00----
SixLaborsCalculate.NET 4.7.21024384.86 ns15.64 ns0.857 ns0.490.03----
SharpZipLibCalculate.NET Core 2.11024790.31 ns353.34 ns19.368 ns1.000.00----
SixLaborsCalculate.NET Core 2.11024465.28 ns652.41 ns35.761 ns0.590.03----
SharpZipLibCalculate.NET Core 3.11024877.25 ns97.89 ns5.365 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102445.60 ns13.28 ns0.728 ns0.050.00----
SharpZipLibCalculate.NET 4.7.220481,537.04 ns428.44 ns23.484 ns1.000.00----
SixLaborsCalculate.NET 4.7.22048849.76 ns1,066.34 ns58.450 ns0.550.04----
SharpZipLibCalculate.NET Core 2.120481,616.97 ns276.70 ns15.167 ns1.000.00----
SixLaborsCalculate.NET Core 2.12048790.77 ns691.71 ns37.915 ns0.490.03----
SharpZipLibCalculate.NET Core 3.120481,735.11 ns1,374.22 ns75.325 ns1.000.00----
SixLaborsCalculate.NET Core 3.1204887.80 ns56.84 ns3.116 ns0.050.00----
SharpZipLibCalculate.NET 4.7.240963,054.53 ns796.41 ns43.654 ns1.000.00----
SixLaborsCalculate.NET 4.7.240961,538.90 ns487.02 ns26.695 ns0.500.01----
SharpZipLibCalculate.NET Core 2.140963,223.48 ns32.32 ns1.771 ns1.000.00----
SixLaborsCalculate.NET Core 2.140961,547.60 ns309.72 ns16.977 ns0.480.01----
SharpZipLibCalculate.NET Core 3.140963,672.33 ns1,095.81 ns60.065 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096159.44 ns36.31 ns1.990 ns0.040.00----

Crc32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.210243,067.24 ns769.25 ns42.165 ns1.000.00----
SixLaborsCalculate.NET 4.7.210242,546.86 ns1,106.36 ns60.643 ns0.830.02----
SharpZipLibCalculate.NET Core 2.110243,377.15 ns3,903.41 ns213.959 ns1.000.00----
SixLaborsCalculate.NET Core 2.110242,524.25 ns2,220.97 ns121.739 ns0.750.04----
SharpZipLibCalculate.NET Core 3.110243,980.60 ns8,497.37 ns465.769 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102478.68 ns69.82 ns3.827 ns0.020.00----
SharpZipLibCalculate.NET 4.7.220487,934.29 ns42,550.13 ns2,332.316 ns1.000.00----
SixLaborsCalculate.NET 4.7.220485,437.81 ns12,760.51 ns699.447 ns0.710.10----
SharpZipLibCalculate.NET Core 2.120486,008.05 ns621.37 ns34.059 ns1.000.00----
SixLaborsCalculate.NET Core 2.120484,791.50 ns3,894.94 ns213.495 ns0.800.04----
SharpZipLibCalculate.NET Core 3.120485,900.06 ns1,344.70 ns73.707 ns1.000.00----
SixLaborsCalculate.NET Core 3.12048103.12 ns15.66 ns0.859 ns0.020.00----
SharpZipLibCalculate.NET 4.7.2409612,422.59 ns1,308.01 ns71.696 ns1.000.00----
SixLaborsCalculate.NET 4.7.2409610,524.63 ns6,267.56 ns343.546 ns0.850.03----
SharpZipLibCalculate.NET Core 2.1409611,888.00 ns1,059.25 ns58.061 ns1.000.00----
SixLaborsCalculate.NET Core 2.140969,806.24 ns241.91 ns13.260 ns0.820.00----
SharpZipLibCalculate.NET Core 3.1409612,181.28 ns1,974.68 ns108.239 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096192.39 ns10.27 ns0.563 ns0.020.00----

@saucecontrolsaucecontrol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool to see this happening! I left a few suggestions for improvements if you want to push it further.

const byte S2301 = 0b1011_0001; // A B C D -> B A D C
const byte S1032 = 0b0100_1110; // A B C D -> C D A B

v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

This was a mistake in the Chromium code. The odd elements of the s1 vector are always 0, so this shuffle/add pair doesn't do anything.

Comment on lines +109 to +111
Vector128<int> v_ps = Vector128.CreateScalar(s1 * n).AsInt32();
Vector128<int> v_s2 = Vector128.CreateScalar(s2).AsInt32();
Vector128<int> v_s1 = Vector128<int>.Zero;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Vector128<int>v_ps=Vector128.CreateScalar(s1*n).AsInt32();
Vector128<int>v_s2=Vector128.CreateScalar(s2).AsInt32();
Vector128<int>v_s1=Vector128<int>.Zero;
Vector128<uint>v_ps=Vector128.CreateScalar(s1*n);
Vector128<uint>v_s2=Vector128.CreateScalar(s2);
Vector128<uint>v_s1=Vector128<uint>.Zero;

The logic depends on these values not overflowing uint.MaxValue when processing NMAX bytes. Best to keep them unsigned all the way through.

Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsUInt32());


// Horizontally add the bytes for s1, multiply-adds the
// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsUInt32());

v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S2301));
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S1032));

s2 = (uint)v_s2.ToScalar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s2=(uint)v_s2.ToScalar();
s2=v_s2.ToScalar();

// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones));
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones).AsUInt32());

Comment on lines +92 to +93
var tap1 = Vector128.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17);
var tap2 = Vector128.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vector128.Create() with a lot of elements is quite inefficient on netcoreapp3.x. @tannergooding recently fixed it up for net5.0 in dotnet/runtime#35857, but you might want to use the ROS trick to load these from fixed data since 3.1 will be around for a while.

Comment on lines +162 to +167
if (length >= 16)
{
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an odd choice by the Chromium dev(s). Since NMAX is an even multiple of 16 but not of 32, you could always safely process a 16-byte straggler as a half iteration of the SIMD loop. No need for this big manually-unrolled block.

For that matter, if the typical data length is greater than, say, 128 bytes the algorithm would extend naturally to AVX2, which would allow 64 bytes per iteration using basically the same code.

Comment on lines +234 to +237
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JIT doesn't do great with code like this.

On legacy jit32, it emits this:

 L0031: movzxeax, byte [ebx+eax] L0035: addedx,eax L0037: addecx,edx L0039: moveax,esi L003b: incesi L003c: movzxeax, byte [ebx+eax] L0040: addedx,eax L0042: addecx,edx L0044: moveax,esi L0046: incesi

RyuJIT does a bit better by not actually incrementing index each time, but it emits an extra mov for each block because it's confused by your two variables:

 L0031: movzxr9d, byte ptr [rax+1] L0036: addr9d,ecx L0039: movecx,r9d L003c: addr8d,ecx L003f: movzxr9d, byte ptr [rax+2] L0044: addr9d,ecx L0047: movecx,r9d L004a: addr8d,ecx

Writing it more like the C code:

byte*pbuff=pbuffer+index;if(length>=16){s2+=(s1+=pbuff[0]);s2+=(s1+=pbuff[1]);s2+=(s1+=pbuff[2]); ...}

gives better codegen in both:

 L0042: movzxr9d, byte ptr [rcx+1] L0047: addeax,r9d L004a: addr8d,eax L004d: movzxr9d, byte ptr [rcx+2] L0052: addeax,r9d L0055: addr8d,eax

SharpLab for both variants

It's also not clear that unroll by 16 is ideal for modern processors. This may be an outdated optimization from zlib. Might be worth testing unroll by 8 or even 4 to see how they do if you haven't already.

Comment on lines +83 to +86
fixed (ulong* k1k2Ptr = &k1k2[0])
fixed (ulong* k3k4Ptr = &k3k4[0])
fixed (ulong* k5k0Ptr = &k5k0[0])
fixed (ulong* polyPtr = &poly[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another place where the ROS trick would be of benefit. Instead of 4 separate unmanaged pointers, you could have a single pointer to a block containing all 4 vector values.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code to use a static readonly array as I wasn't sure the ROS optimizations apply for anything other than byte?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the compiler won't optimize anything bigger than byte because of endianness concerns. Since this code is processor-specific, you can outsmart the compiler by breaking the ulongs up into bytes (and reversing their byte order, ofc)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that for now since the massive hashing improvements have actually made less of an impact than I'd hoped. I'm going to profile now and find other soft targets.

@antonfirsovantonfirsov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (without going into implementation details).


// act
crc.Update(data);
// Longer run, enough to require moving the point in SIMD implementation with

@antonfirsovantonfirsovMay 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there there are some other corner cases worth of testing?
Eg: data.Length == 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, The current test isn't great, I'll add better reference tests

@JimBobSquarePants

JimBobSquarePants commented May 18, 2020

Copy link
Copy Markdown
MemberAuthor

@saucecontrol Thanks for the review! Some great improvement ideas there. 👍 I've committed changes in one go rather than accepting individual suggestions as I didn't want to keep triggering the build systems.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@JimBobSquarePants@antonfirsov@saucecontrol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Hardware Accelerated Checksums - #1201

Merged
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash
May 18, 2020
Merged

Add Hardware Accelerated Checksums#1201
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash

Conversation

@JimBobSquarePants

@JimBobSquarePantsJimBobSquarePants commented May 16, 2020

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Added support for hardware accelerated Crc32 and Adler32 checksum generation both used by our png codecs. They're fast!

Benchmarks.

Adler32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.21024793.18 ns775.66 ns42.516 ns1.000.00----
SixLaborsCalculate.NET 4.7.21024384.86 ns15.64 ns0.857 ns0.490.03----
SharpZipLibCalculate.NET Core 2.11024790.31 ns353.34 ns19.368 ns1.000.00----
SixLaborsCalculate.NET Core 2.11024465.28 ns652.41 ns35.761 ns0.590.03----
SharpZipLibCalculate.NET Core 3.11024877.25 ns97.89 ns5.365 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102445.60 ns13.28 ns0.728 ns0.050.00----
SharpZipLibCalculate.NET 4.7.220481,537.04 ns428.44 ns23.484 ns1.000.00----
SixLaborsCalculate.NET 4.7.22048849.76 ns1,066.34 ns58.450 ns0.550.04----
SharpZipLibCalculate.NET Core 2.120481,616.97 ns276.70 ns15.167 ns1.000.00----
SixLaborsCalculate.NET Core 2.12048790.77 ns691.71 ns37.915 ns0.490.03----
SharpZipLibCalculate.NET Core 3.120481,735.11 ns1,374.22 ns75.325 ns1.000.00----
SixLaborsCalculate.NET Core 3.1204887.80 ns56.84 ns3.116 ns0.050.00----
SharpZipLibCalculate.NET 4.7.240963,054.53 ns796.41 ns43.654 ns1.000.00----
SixLaborsCalculate.NET 4.7.240961,538.90 ns487.02 ns26.695 ns0.500.01----
SharpZipLibCalculate.NET Core 2.140963,223.48 ns32.32 ns1.771 ns1.000.00----
SixLaborsCalculate.NET Core 2.140961,547.60 ns309.72 ns16.977 ns0.480.01----
SharpZipLibCalculate.NET Core 3.140963,672.33 ns1,095.81 ns60.065 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096159.44 ns36.31 ns1.990 ns0.040.00----

Crc32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.210243,067.24 ns769.25 ns42.165 ns1.000.00----
SixLaborsCalculate.NET 4.7.210242,546.86 ns1,106.36 ns60.643 ns0.830.02----
SharpZipLibCalculate.NET Core 2.110243,377.15 ns3,903.41 ns213.959 ns1.000.00----
SixLaborsCalculate.NET Core 2.110242,524.25 ns2,220.97 ns121.739 ns0.750.04----
SharpZipLibCalculate.NET Core 3.110243,980.60 ns8,497.37 ns465.769 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102478.68 ns69.82 ns3.827 ns0.020.00----
SharpZipLibCalculate.NET 4.7.220487,934.29 ns42,550.13 ns2,332.316 ns1.000.00----
SixLaborsCalculate.NET 4.7.220485,437.81 ns12,760.51 ns699.447 ns0.710.10----
SharpZipLibCalculate.NET Core 2.120486,008.05 ns621.37 ns34.059 ns1.000.00----
SixLaborsCalculate.NET Core 2.120484,791.50 ns3,894.94 ns213.495 ns0.800.04----
SharpZipLibCalculate.NET Core 3.120485,900.06 ns1,344.70 ns73.707 ns1.000.00----
SixLaborsCalculate.NET Core 3.12048103.12 ns15.66 ns0.859 ns0.020.00----
SharpZipLibCalculate.NET 4.7.2409612,422.59 ns1,308.01 ns71.696 ns1.000.00----
SixLaborsCalculate.NET 4.7.2409610,524.63 ns6,267.56 ns343.546 ns0.850.03----
SharpZipLibCalculate.NET Core 2.1409611,888.00 ns1,059.25 ns58.061 ns1.000.00----
SixLaborsCalculate.NET Core 2.140969,806.24 ns241.91 ns13.260 ns0.820.00----
SharpZipLibCalculate.NET Core 3.1409612,181.28 ns1,974.68 ns108.239 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096192.39 ns10.27 ns0.563 ns0.020.00----

@saucecontrolsaucecontrol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool to see this happening! I left a few suggestions for improvements if you want to push it further.

const byte S2301 = 0b1011_0001; // A B C D -> B A D C
const byte S1032 = 0b0100_1110; // A B C D -> C D A B

v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

This was a mistake in the Chromium code. The odd elements of the s1 vector are always 0, so this shuffle/add pair doesn't do anything.

Comment on lines +109 to +111
Vector128<int> v_ps = Vector128.CreateScalar(s1 * n).AsInt32();
Vector128<int> v_s2 = Vector128.CreateScalar(s2).AsInt32();
Vector128<int> v_s1 = Vector128<int>.Zero;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Vector128<int>v_ps=Vector128.CreateScalar(s1*n).AsInt32();
Vector128<int>v_s2=Vector128.CreateScalar(s2).AsInt32();
Vector128<int>v_s1=Vector128<int>.Zero;
Vector128<uint>v_ps=Vector128.CreateScalar(s1*n);
Vector128<uint>v_s2=Vector128.CreateScalar(s2);
Vector128<uint>v_s1=Vector128<uint>.Zero;

The logic depends on these values not overflowing uint.MaxValue when processing NMAX bytes. Best to keep them unsigned all the way through.

Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsUInt32());


// Horizontally add the bytes for s1, multiply-adds the
// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsUInt32());

v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S2301));
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S1032));

s2 = (uint)v_s2.ToScalar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s2=(uint)v_s2.ToScalar();
s2=v_s2.ToScalar();

// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones));
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones).AsUInt32());

Comment on lines +92 to +93
var tap1 = Vector128.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17);
var tap2 = Vector128.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vector128.Create() with a lot of elements is quite inefficient on netcoreapp3.x. @tannergooding recently fixed it up for net5.0 in dotnet/runtime#35857, but you might want to use the ROS trick to load these from fixed data since 3.1 will be around for a while.

Comment on lines +162 to +167
if (length >= 16)
{
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an odd choice by the Chromium dev(s). Since NMAX is an even multiple of 16 but not of 32, you could always safely process a 16-byte straggler as a half iteration of the SIMD loop. No need for this big manually-unrolled block.

For that matter, if the typical data length is greater than, say, 128 bytes the algorithm would extend naturally to AVX2, which would allow 64 bytes per iteration using basically the same code.

Comment on lines +234 to +237
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JIT doesn't do great with code like this.

On legacy jit32, it emits this:

 L0031: movzxeax, byte [ebx+eax] L0035: addedx,eax L0037: addecx,edx L0039: moveax,esi L003b: incesi L003c: movzxeax, byte [ebx+eax] L0040: addedx,eax L0042: addecx,edx L0044: moveax,esi L0046: incesi

RyuJIT does a bit better by not actually incrementing index each time, but it emits an extra mov for each block because it's confused by your two variables:

 L0031: movzxr9d, byte ptr [rax+1] L0036: addr9d,ecx L0039: movecx,r9d L003c: addr8d,ecx L003f: movzxr9d, byte ptr [rax+2] L0044: addr9d,ecx L0047: movecx,r9d L004a: addr8d,ecx

Writing it more like the C code:

byte*pbuff=pbuffer+index;if(length>=16){s2+=(s1+=pbuff[0]);s2+=(s1+=pbuff[1]);s2+=(s1+=pbuff[2]); ...}

gives better codegen in both:

 L0042: movzxr9d, byte ptr [rcx+1] L0047: addeax,r9d L004a: addr8d,eax L004d: movzxr9d, byte ptr [rcx+2] L0052: addeax,r9d L0055: addr8d,eax

SharpLab for both variants

It's also not clear that unroll by 16 is ideal for modern processors. This may be an outdated optimization from zlib. Might be worth testing unroll by 8 or even 4 to see how they do if you haven't already.

Comment on lines +83 to +86
fixed (ulong* k1k2Ptr = &k1k2[0])
fixed (ulong* k3k4Ptr = &k3k4[0])
fixed (ulong* k5k0Ptr = &k5k0[0])
fixed (ulong* polyPtr = &poly[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another place where the ROS trick would be of benefit. Instead of 4 separate unmanaged pointers, you could have a single pointer to a block containing all 4 vector values.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code to use a static readonly array as I wasn't sure the ROS optimizations apply for anything other than byte?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the compiler won't optimize anything bigger than byte because of endianness concerns. Since this code is processor-specific, you can outsmart the compiler by breaking the ulongs up into bytes (and reversing their byte order, ofc)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that for now since the massive hashing improvements have actually made less of an impact than I'd hoped. I'm going to profile now and find other soft targets.

@antonfirsovantonfirsov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (without going into implementation details).


// act
crc.Update(data);
// Longer run, enough to require moving the point in SIMD implementation with

@antonfirsovantonfirsovMay 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there there are some other corner cases worth of testing?
Eg: data.Length == 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, The current test isn't great, I'll add better reference tests

@JimBobSquarePants

JimBobSquarePants commented May 18, 2020

Copy link
Copy Markdown
MemberAuthor

@saucecontrol Thanks for the review! Some great improvement ideas there. 👍 I've committed changes in one go rather than accepting individual suggestions as I didn't want to keep triggering the build systems.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@JimBobSquarePants@antonfirsov@saucecontrol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Hardware Accelerated Checksums - #1201

Merged
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash
May 18, 2020
Merged

Add Hardware Accelerated Checksums#1201
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash

Conversation

@JimBobSquarePants

@JimBobSquarePantsJimBobSquarePants commented May 16, 2020

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Added support for hardware accelerated Crc32 and Adler32 checksum generation both used by our png codecs. They're fast!

Benchmarks.

Adler32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.21024793.18 ns775.66 ns42.516 ns1.000.00----
SixLaborsCalculate.NET 4.7.21024384.86 ns15.64 ns0.857 ns0.490.03----
SharpZipLibCalculate.NET Core 2.11024790.31 ns353.34 ns19.368 ns1.000.00----
SixLaborsCalculate.NET Core 2.11024465.28 ns652.41 ns35.761 ns0.590.03----
SharpZipLibCalculate.NET Core 3.11024877.25 ns97.89 ns5.365 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102445.60 ns13.28 ns0.728 ns0.050.00----
SharpZipLibCalculate.NET 4.7.220481,537.04 ns428.44 ns23.484 ns1.000.00----
SixLaborsCalculate.NET 4.7.22048849.76 ns1,066.34 ns58.450 ns0.550.04----
SharpZipLibCalculate.NET Core 2.120481,616.97 ns276.70 ns15.167 ns1.000.00----
SixLaborsCalculate.NET Core 2.12048790.77 ns691.71 ns37.915 ns0.490.03----
SharpZipLibCalculate.NET Core 3.120481,735.11 ns1,374.22 ns75.325 ns1.000.00----
SixLaborsCalculate.NET Core 3.1204887.80 ns56.84 ns3.116 ns0.050.00----
SharpZipLibCalculate.NET 4.7.240963,054.53 ns796.41 ns43.654 ns1.000.00----
SixLaborsCalculate.NET 4.7.240961,538.90 ns487.02 ns26.695 ns0.500.01----
SharpZipLibCalculate.NET Core 2.140963,223.48 ns32.32 ns1.771 ns1.000.00----
SixLaborsCalculate.NET Core 2.140961,547.60 ns309.72 ns16.977 ns0.480.01----
SharpZipLibCalculate.NET Core 3.140963,672.33 ns1,095.81 ns60.065 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096159.44 ns36.31 ns1.990 ns0.040.00----

Crc32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.210243,067.24 ns769.25 ns42.165 ns1.000.00----
SixLaborsCalculate.NET 4.7.210242,546.86 ns1,106.36 ns60.643 ns0.830.02----
SharpZipLibCalculate.NET Core 2.110243,377.15 ns3,903.41 ns213.959 ns1.000.00----
SixLaborsCalculate.NET Core 2.110242,524.25 ns2,220.97 ns121.739 ns0.750.04----
SharpZipLibCalculate.NET Core 3.110243,980.60 ns8,497.37 ns465.769 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102478.68 ns69.82 ns3.827 ns0.020.00----
SharpZipLibCalculate.NET 4.7.220487,934.29 ns42,550.13 ns2,332.316 ns1.000.00----
SixLaborsCalculate.NET 4.7.220485,437.81 ns12,760.51 ns699.447 ns0.710.10----
SharpZipLibCalculate.NET Core 2.120486,008.05 ns621.37 ns34.059 ns1.000.00----
SixLaborsCalculate.NET Core 2.120484,791.50 ns3,894.94 ns213.495 ns0.800.04----
SharpZipLibCalculate.NET Core 3.120485,900.06 ns1,344.70 ns73.707 ns1.000.00----
SixLaborsCalculate.NET Core 3.12048103.12 ns15.66 ns0.859 ns0.020.00----
SharpZipLibCalculate.NET 4.7.2409612,422.59 ns1,308.01 ns71.696 ns1.000.00----
SixLaborsCalculate.NET 4.7.2409610,524.63 ns6,267.56 ns343.546 ns0.850.03----
SharpZipLibCalculate.NET Core 2.1409611,888.00 ns1,059.25 ns58.061 ns1.000.00----
SixLaborsCalculate.NET Core 2.140969,806.24 ns241.91 ns13.260 ns0.820.00----
SharpZipLibCalculate.NET Core 3.1409612,181.28 ns1,974.68 ns108.239 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096192.39 ns10.27 ns0.563 ns0.020.00----

@saucecontrolsaucecontrol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool to see this happening! I left a few suggestions for improvements if you want to push it further.

const byte S2301 = 0b1011_0001; // A B C D -> B A D C
const byte S1032 = 0b0100_1110; // A B C D -> C D A B

v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

This was a mistake in the Chromium code. The odd elements of the s1 vector are always 0, so this shuffle/add pair doesn't do anything.

Comment on lines +109 to +111
Vector128<int> v_ps = Vector128.CreateScalar(s1 * n).AsInt32();
Vector128<int> v_s2 = Vector128.CreateScalar(s2).AsInt32();
Vector128<int> v_s1 = Vector128<int>.Zero;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Vector128<int>v_ps=Vector128.CreateScalar(s1*n).AsInt32();
Vector128<int>v_s2=Vector128.CreateScalar(s2).AsInt32();
Vector128<int>v_s1=Vector128<int>.Zero;
Vector128<uint>v_ps=Vector128.CreateScalar(s1*n);
Vector128<uint>v_s2=Vector128.CreateScalar(s2);
Vector128<uint>v_s1=Vector128<uint>.Zero;

The logic depends on these values not overflowing uint.MaxValue when processing NMAX bytes. Best to keep them unsigned all the way through.

Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsUInt32());


// Horizontally add the bytes for s1, multiply-adds the
// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsUInt32());

v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S2301));
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S1032));

s2 = (uint)v_s2.ToScalar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s2=(uint)v_s2.ToScalar();
s2=v_s2.ToScalar();

// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones));
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones).AsUInt32());

Comment on lines +92 to +93
var tap1 = Vector128.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17);
var tap2 = Vector128.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vector128.Create() with a lot of elements is quite inefficient on netcoreapp3.x. @tannergooding recently fixed it up for net5.0 in dotnet/runtime#35857, but you might want to use the ROS trick to load these from fixed data since 3.1 will be around for a while.

Comment on lines +162 to +167
if (length >= 16)
{
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an odd choice by the Chromium dev(s). Since NMAX is an even multiple of 16 but not of 32, you could always safely process a 16-byte straggler as a half iteration of the SIMD loop. No need for this big manually-unrolled block.

For that matter, if the typical data length is greater than, say, 128 bytes the algorithm would extend naturally to AVX2, which would allow 64 bytes per iteration using basically the same code.

Comment on lines +234 to +237
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JIT doesn't do great with code like this.

On legacy jit32, it emits this:

 L0031: movzxeax, byte [ebx+eax] L0035: addedx,eax L0037: addecx,edx L0039: moveax,esi L003b: incesi L003c: movzxeax, byte [ebx+eax] L0040: addedx,eax L0042: addecx,edx L0044: moveax,esi L0046: incesi

RyuJIT does a bit better by not actually incrementing index each time, but it emits an extra mov for each block because it's confused by your two variables:

 L0031: movzxr9d, byte ptr [rax+1] L0036: addr9d,ecx L0039: movecx,r9d L003c: addr8d,ecx L003f: movzxr9d, byte ptr [rax+2] L0044: addr9d,ecx L0047: movecx,r9d L004a: addr8d,ecx

Writing it more like the C code:

byte*pbuff=pbuffer+index;if(length>=16){s2+=(s1+=pbuff[0]);s2+=(s1+=pbuff[1]);s2+=(s1+=pbuff[2]); ...}

gives better codegen in both:

 L0042: movzxr9d, byte ptr [rcx+1] L0047: addeax,r9d L004a: addr8d,eax L004d: movzxr9d, byte ptr [rcx+2] L0052: addeax,r9d L0055: addr8d,eax

SharpLab for both variants

It's also not clear that unroll by 16 is ideal for modern processors. This may be an outdated optimization from zlib. Might be worth testing unroll by 8 or even 4 to see how they do if you haven't already.

Comment on lines +83 to +86
fixed (ulong* k1k2Ptr = &k1k2[0])
fixed (ulong* k3k4Ptr = &k3k4[0])
fixed (ulong* k5k0Ptr = &k5k0[0])
fixed (ulong* polyPtr = &poly[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another place where the ROS trick would be of benefit. Instead of 4 separate unmanaged pointers, you could have a single pointer to a block containing all 4 vector values.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code to use a static readonly array as I wasn't sure the ROS optimizations apply for anything other than byte?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the compiler won't optimize anything bigger than byte because of endianness concerns. Since this code is processor-specific, you can outsmart the compiler by breaking the ulongs up into bytes (and reversing their byte order, ofc)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that for now since the massive hashing improvements have actually made less of an impact than I'd hoped. I'm going to profile now and find other soft targets.

@antonfirsovantonfirsov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (without going into implementation details).


// act
crc.Update(data);
// Longer run, enough to require moving the point in SIMD implementation with

@antonfirsovantonfirsovMay 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there there are some other corner cases worth of testing?
Eg: data.Length == 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, The current test isn't great, I'll add better reference tests

@JimBobSquarePants

JimBobSquarePants commented May 18, 2020

Copy link
Copy Markdown
MemberAuthor

@saucecontrol Thanks for the review! Some great improvement ideas there. 👍 I've committed changes in one go rather than accepting individual suggestions as I didn't want to keep triggering the build systems.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@JimBobSquarePants@antonfirsov@saucecontrol
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add Hardware Accelerated Checksums - #1201

Merged
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash
May 18, 2020
Merged

Add Hardware Accelerated Checksums#1201
JimBobSquarePants merged 6 commits into
masterfrom
js/fast-hash

Conversation

@JimBobSquarePants

@JimBobSquarePantsJimBobSquarePants commented May 16, 2020

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Added support for hardware accelerated Crc32 and Adler32 checksum generation both used by our png codecs. They're fast!

Benchmarks.

Adler32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.21024793.18 ns775.66 ns42.516 ns1.000.00----
SixLaborsCalculate.NET 4.7.21024384.86 ns15.64 ns0.857 ns0.490.03----
SharpZipLibCalculate.NET Core 2.11024790.31 ns353.34 ns19.368 ns1.000.00----
SixLaborsCalculate.NET Core 2.11024465.28 ns652.41 ns35.761 ns0.590.03----
SharpZipLibCalculate.NET Core 3.11024877.25 ns97.89 ns5.365 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102445.60 ns13.28 ns0.728 ns0.050.00----
SharpZipLibCalculate.NET 4.7.220481,537.04 ns428.44 ns23.484 ns1.000.00----
SixLaborsCalculate.NET 4.7.22048849.76 ns1,066.34 ns58.450 ns0.550.04----
SharpZipLibCalculate.NET Core 2.120481,616.97 ns276.70 ns15.167 ns1.000.00----
SixLaborsCalculate.NET Core 2.12048790.77 ns691.71 ns37.915 ns0.490.03----
SharpZipLibCalculate.NET Core 3.120481,735.11 ns1,374.22 ns75.325 ns1.000.00----
SixLaborsCalculate.NET Core 3.1204887.80 ns56.84 ns3.116 ns0.050.00----
SharpZipLibCalculate.NET 4.7.240963,054.53 ns796.41 ns43.654 ns1.000.00----
SixLaborsCalculate.NET 4.7.240961,538.90 ns487.02 ns26.695 ns0.500.01----
SharpZipLibCalculate.NET Core 2.140963,223.48 ns32.32 ns1.771 ns1.000.00----
SixLaborsCalculate.NET Core 2.140961,547.60 ns309.72 ns16.977 ns0.480.01----
SharpZipLibCalculate.NET Core 3.140963,672.33 ns1,095.81 ns60.065 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096159.44 ns36.31 ns1.990 ns0.040.00----

Crc32

MethodRuntimeCountMeanErrorStdDevRatioRatioSDGen 0Gen 1Gen 2Allocated
SharpZipLibCalculate.NET 4.7.210243,067.24 ns769.25 ns42.165 ns1.000.00----
SixLaborsCalculate.NET 4.7.210242,546.86 ns1,106.36 ns60.643 ns0.830.02----
SharpZipLibCalculate.NET Core 2.110243,377.15 ns3,903.41 ns213.959 ns1.000.00----
SixLaborsCalculate.NET Core 2.110242,524.25 ns2,220.97 ns121.739 ns0.750.04----
SharpZipLibCalculate.NET Core 3.110243,980.60 ns8,497.37 ns465.769 ns1.000.00----
SixLaborsCalculate.NET Core 3.1102478.68 ns69.82 ns3.827 ns0.020.00----
SharpZipLibCalculate.NET 4.7.220487,934.29 ns42,550.13 ns2,332.316 ns1.000.00----
SixLaborsCalculate.NET 4.7.220485,437.81 ns12,760.51 ns699.447 ns0.710.10----
SharpZipLibCalculate.NET Core 2.120486,008.05 ns621.37 ns34.059 ns1.000.00----
SixLaborsCalculate.NET Core 2.120484,791.50 ns3,894.94 ns213.495 ns0.800.04----
SharpZipLibCalculate.NET Core 3.120485,900.06 ns1,344.70 ns73.707 ns1.000.00----
SixLaborsCalculate.NET Core 3.12048103.12 ns15.66 ns0.859 ns0.020.00----
SharpZipLibCalculate.NET 4.7.2409612,422.59 ns1,308.01 ns71.696 ns1.000.00----
SixLaborsCalculate.NET 4.7.2409610,524.63 ns6,267.56 ns343.546 ns0.850.03----
SharpZipLibCalculate.NET Core 2.1409611,888.00 ns1,059.25 ns58.061 ns1.000.00----
SixLaborsCalculate.NET Core 2.140969,806.24 ns241.91 ns13.260 ns0.820.00----
SharpZipLibCalculate.NET Core 3.1409612,181.28 ns1,974.68 ns108.239 ns1.000.00----
SixLaborsCalculate.NET Core 3.14096192.39 ns10.27 ns0.563 ns0.020.00----

@saucecontrolsaucecontrol left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool to see this happening! I left a few suggestions for improvements if you want to push it further.

const byte S2301 = 0b1011_0001; // A B C D -> B A D C
const byte S1032 = 0b0100_1110; // A B C D -> C D A B

v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, S2301));

This was a mistake in the Chromium code. The odd elements of the s1 vector are always 0, so this shuffle/add pair doesn't do anything.

Comment on lines +109 to +111
Vector128<int> v_ps = Vector128.CreateScalar(s1 * n).AsInt32();
Vector128<int> v_s2 = Vector128.CreateScalar(s2).AsInt32();
Vector128<int> v_s1 = Vector128<int>.Zero;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Vector128<int>v_ps=Vector128.CreateScalar(s1*n).AsInt32();
Vector128<int>v_s2=Vector128.CreateScalar(s2).AsInt32();
Vector128<int>v_s1=Vector128<int>.Zero;
Vector128<uint>v_ps=Vector128.CreateScalar(s1*n);
Vector128<uint>v_s2=Vector128.CreateScalar(s2);
Vector128<uint>v_s1=Vector128<uint>.Zero;

The logic depends on these values not overflowing uint.MaxValue when processing NMAX bytes. Best to keep them unsigned all the way through.

Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes2,zero).AsUInt32());


// Horizontally add the bytes for s1, multiply-adds the
// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsInt32());
v_s1=Sse2.Add(v_s1,Sse2.SumAbsoluteDifferences(bytes1,zero).AsUInt32());

v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S2301));
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, S1032));

s2 = (uint)v_s2.ToScalar();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
s2=(uint)v_s2.ToScalar();
s2=v_s2.ToScalar();

// bytes by [ 32, 31, 30, ... ] for s2.
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsInt32());
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1);
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones));
v_s2=Sse2.Add(v_s2,Sse2.MultiplyAddAdjacent(mad1,ones).AsUInt32());

Comment on lines +92 to +93
var tap1 = Vector128.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17);
var tap2 = Vector128.Create(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vector128.Create() with a lot of elements is quite inefficient on netcoreapp3.x. @tannergooding recently fixed it up for net5.0 in dotnet/runtime#35857, but you might want to use the ROS trick to load these from fixed data since 3.1 will be around for a while.

Comment on lines +162 to +167
if (length >= 16)
{
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an odd choice by the Chromium dev(s). Since NMAX is an even multiple of 16 but not of 32, you could always safely process a 16-byte straggler as a half iteration of the SIMD loop. No need for this big manually-unrolled block.

For that matter, if the typical data length is greater than, say, 128 bytes the algorithm would extend naturally to AVX2, which would allow 64 bytes per iteration using basically the same code.

Comment on lines +234 to +237
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;
s1 += Unsafe.Add(ref bufferRef, index++);
s2 += s1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JIT doesn't do great with code like this.

On legacy jit32, it emits this:

 L0031: movzxeax, byte [ebx+eax] L0035: addedx,eax L0037: addecx,edx L0039: moveax,esi L003b: incesi L003c: movzxeax, byte [ebx+eax] L0040: addedx,eax L0042: addecx,edx L0044: moveax,esi L0046: incesi

RyuJIT does a bit better by not actually incrementing index each time, but it emits an extra mov for each block because it's confused by your two variables:

 L0031: movzxr9d, byte ptr [rax+1] L0036: addr9d,ecx L0039: movecx,r9d L003c: addr8d,ecx L003f: movzxr9d, byte ptr [rax+2] L0044: addr9d,ecx L0047: movecx,r9d L004a: addr8d,ecx

Writing it more like the C code:

byte*pbuff=pbuffer+index;if(length>=16){s2+=(s1+=pbuff[0]);s2+=(s1+=pbuff[1]);s2+=(s1+=pbuff[2]); ...}

gives better codegen in both:

 L0042: movzxr9d, byte ptr [rcx+1] L0047: addeax,r9d L004a: addr8d,eax L004d: movzxr9d, byte ptr [rcx+2] L0052: addeax,r9d L0055: addr8d,eax

SharpLab for both variants

It's also not clear that unroll by 16 is ideal for modern processors. This may be an outdated optimization from zlib. Might be worth testing unroll by 8 or even 4 to see how they do if you haven't already.

Comment on lines +83 to +86
fixed (ulong* k1k2Ptr = &k1k2[0])
fixed (ulong* k3k4Ptr = &k3k4[0])
fixed (ulong* k5k0Ptr = &k5k0[0])
fixed (ulong* polyPtr = &poly[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another place where the ROS trick would be of benefit. Instead of 4 separate unmanaged pointers, you could have a single pointer to a block containing all 4 vector values.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code to use a static readonly array as I wasn't sure the ROS optimizations apply for anything other than byte?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the compiler won't optimize anything bigger than byte because of endianness concerns. Since this code is processor-specific, you can outsmart the compiler by breaking the ulongs up into bytes (and reversing their byte order, ofc)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that for now since the massive hashing improvements have actually made less of an impact than I'd hoped. I'm going to profile now and find other soft targets.

@antonfirsovantonfirsov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (without going into implementation details).


// act
crc.Update(data);
// Longer run, enough to require moving the point in SIMD implementation with

@antonfirsovantonfirsovMay 18, 2020

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there there are some other corner cases worth of testing?
Eg: data.Length == 0

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, The current test isn't great, I'll add better reference tests

@JimBobSquarePants

JimBobSquarePants commented May 18, 2020

Copy link
Copy Markdown
MemberAuthor

@saucecontrol Thanks for the review! Some great improvement ideas there. 👍 I've committed changes in one go rather than accepting individual suggestions as I didn't want to keep triggering the build systems.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@JimBobSquarePants@antonfirsov@saucecontrol