Added SIMD path for Vector3, Quaternion and Matrix4x4 operations - #99547

Closed
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd
Closed

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations#99547
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd

Conversation

@martenf

@martenfmartenf commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

Added SIMD path using Vector128 (and Vector256 for Matrix4x4) for:

Vector3.Cross(Vector3 vector1, Vector3 vector2)

MethodMeanErrorStdDevRatioRatioSD
Before1.3289 ns0.0531 ns0.0708 ns1.000.00
After0.6613 ns0.0399 ns0.0833 ns0.510.05

Vector3.Transform(Vector3 value, Quaternion rotation)

MethodMeanErrorStdDevRatioRatioSD
Before3.922 ns0.1034 ns0.1380 ns1.000.00
After1.258 ns0.0508 ns0.0776 ns0.320.02

Quaternion operator *(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
dotnet84.0531 ns0.1008 ns0.0990 ns1.000.00
dotnet90.7203 ns0.0312 ns0.0292 ns0.180.01
after0.5752 ns0.0185 ns0.0173 ns0.140.00

Quaternion operator /(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before6.010 ns0.1399 ns0.1240 ns1.000.00
after2.377 ns0.0334 ns0.0296 ns0.400.01

Quaternion Concatenate(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before4.166 ns0.0344 ns0.0321 ns0.690.02
after1.266 ns0.0443 ns0.0414 ns0.210.01

Impl operator *(in Impl left, in Impl right)

MethodMeanErrorStdDevRatio
before8.009 ns0.0856 ns0.0801 ns1.00
afterVector1285.407 ns0.0604 ns0.0565 ns0.68
afterVector2563.142 ns0.0616 ns0.0546 ns0.39

Vector3.Cross(Vector3 vector1, Vector3 vector2)
Vector3.Transform(Vector3 value, Quaternion rotation)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

added simd paths for quaternion multiplication, division and concatenate
added simd path for matrix4x4 multiplication
@martenfmartenf changed the title Added SIMD path for Vector3.Cross and Vector3.TransformAdded SIMD path for Vector3, Quaternion and Matrix4x4 operationsMar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet/area-system-numerics

Comment on lines +531 to +546
if (Vector128.IsHardwareAccelerated)
{
var vVector = value.AsVector128();
var rVector = Unsafe.BitCast<Quaternion, Vector128<float>>(rotation);

// v + 2 * (q x (q.W * v + q x v)
return (vVector + Vector128.Create(2f) * Cross(rVector, Vector128.Shuffle(rVector, Vector128.Create(3, 3, 3, 3)) * vVector + Cross(rVector, vVector))).AsVector3();

static Vector128<float> Cross(Vector128<float> v1, Vector128<float> v2)
{
return (Vector128.Shuffle(v1, Vector128.Create(1, 2, 0, 3)) *
Vector128.Shuffle(v2, Vector128.Create(2, 0, 1, 3))) -
(Vector128.Shuffle(v1, Vector128.Create(2, 0, 1, 3)) *
Vector128.Shuffle(v2, Vector128.Create(1, 2, 0, 3)));
}
}

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.

I'm not immediately following where this implementation is from...

The standard naive algorithm is typically broken down to something like:

return((Quaternion.Conjugate(rotation)*newQuaternion(value,0.0f))*rotation).AsVector128().AsVector3();

(noting that Quaternion.operator *(Quaternion, Quaterion) is not currently marked as aggressively inline)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I got it from a private C library I wrote a decade ago which quotes a dead link.
The closest I could find on the internet after a quick search is this forum post which gets 95% of the way there and just misses the last 'simplification for readability' step.

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.

Can we update this to follow the "standard" algorithm I gave above.

I expect we'll get better support for things like constant folding, more consistent performance across a range of hardware, and we can accurately quote the root implementation to a known source (which avoids any potential licensing issues): https://github.com/microsoft/DirectXMath/blob/main/Inc/DirectXMathVector.inl#L10307-L10317

IIRC, glm uses a similar algorithm for it's implementation

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication. It will also always be slower then the reduced version because it contains significantly more math operation.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

To reduce any variation between different archs the implementation could be changed to

return (vVector + 2 * Cross(rVector, Vector128.Create(rotation.W) * vVector + Cross(rVector, vVector))).AsVector3();

which would give the JIT a bit more leeway, but I doubt it makes any differens.

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.

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication

This is in part because quaternion multiplication is not being inlined currently. But, also because it's doing some "unnecessary" work, as you indicated. But that can be accounted for as well.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

In general we require correct attribution to exist and based on the statement given above, the code (even if you authored it) was itself based on some internet blog/forum/site/etc post that can no longer be referenced (due to a dead link). Such a post may have itself may have been based on something else which overall makes it very difficult for us to use "safely" as the chain of citations is broken.

The actual requirements for attribution, whether or not something can be copyrighted, and the like can get quite complex. When there isn't a clear chain anymore, then it requires additional effort on our part, potentially even requiring checks with legal, so an actual lawyer can make the determination on whether or not it is safe to do.

.NET is a huge open source project depending on by millions. We must do the due diligence and ensure that all the boxes are checked.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

What makes the result of Q*vQ any more correct that any of the other ways you could use to rotate a vector by the rotation saved in a quaternion? Rodrigues' formula is actually older than Hamilton's, but it's not like any of this is a universal law written in the stars. This entire conversation feels pointless so I will stop wasting everyone's time, close this pull request and just use a private library.

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.

From a mathematical perspective they all produce the "same" result. The consideration is that they are not the same from a programmatic perspective due to additional considerations.

The intent was to push this PR in a direction where we could still improve the performance but without changing certain invariants that are extremely important to maintain. That requires more in depth consideration and documentation to show how those requirements are being met.

The intent was not to waste time or get into an argument over what is right vs wrong. The contribution was appreciated and was otherwise nearly in the right shape to be taken.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I know and appreciate that you weren't trying to waste any time, but I just don't have the spare time to have long debates about computational vs mathematical correctness right now, so I think it's just easier if I leave it at that so that someone else can make a pull request if they run into the same bottlenecks.

@tannergoodingtannergoodingJun 14, 2024

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.

👍, are you fine with me cherry-picking your commits into a PR so I can fix the last couple bits of feedback and get merged? That way you can still get credit for the overall contribution

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment on lines +163 to +178
var l0012 = Vector128.Shuffle(left, Vector128.Create(0, 0, 1, 2));
var l1120 = Vector128.Shuffle(left, Vector128.Create(1, 1, 2, 0));
var r0333 = Vector128.Shuffle(right, Vector128.Create(0, 3, 3, 3));
var r1201 = Vector128.Shuffle(right, Vector128.Create(1, 2, 0, 1));

var t0 = l0012 * r0333 + l1120 * r1201;
var mask = Vector128.Create(0x80000000, 0, 0, 0);
var t0m = Vector128.Xor(t0, mask.AsSingle());

var l2201 = Vector128.Shuffle(left, Vector128.Create(2, 2, 0, 1));
var l3333 = Vector128.Shuffle(left, Vector128.Create(3, 3, 3, 3));
var r2120 = Vector128.Shuffle(right, Vector128.Create(2, 1, 2, 0));
var r3012 = Vector128.Shuffle(right, Vector128.Create(3, 0, 1, 2));

var t1 = l3333 * r3012 - l2201 * r2120 + t0m;
var result = Vector128.Shuffle(t1, Vector128.Create(1, 2, 3, 0));

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.

This new code is much harder to read for basically a cycle difference in perf (numbers above report 0.7 -> 0.5 ns). It may also not optimize as well when considering all platforms or ISA baselines (SSE2, SSE4.1, AVX2, AVX512F, AdvSimd, WASM, etc).

It's also substantially different from Concatenate despite them doing the "same thing" (just one is x * y and the other is y * x)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's an operation that tends to be in the hot path for 3d applications so thought the reduced readability might be worth the extra performance on all the platforms I tested it on.

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.

Its not that hot for 3D apps. The actual hot code tends to happen on the GPU instead.

Its especially not so hot that the complexity is worth 0.15ns (which is effectively 1 CPU cycle).

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.

This comment is still pending. I don't think the new code is worth the 0.15ns savings (it's notably slightly slower on my box too). The code is more complex, harder to follow, and so I don't think the justification is there to take this part of the change.

.NET is huge, it is millions of lines of code. We ultimately have to balance readability, maintainability, security, perf, etc.

While Vector3 is itself a performance oriented type, we still have to factor in the other considerations and balance them out. It also applies beyond what a simple benchmark may measure, as we have multiple platforms, ISAs, and ABIs to consider. We also have to consider how certain optimizations may function (such as inlining and constant folding).

As such, even with it being a performance oriented type, there's many places where we are happy to give up a half nanosecond (which is in practice 1-5 instruction cycles) to improve the other areas.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
@tannergooding

Copy link
Copy Markdown
Member

@martenf is this something you're still working on? There's still some pending feedback above that needs to be resolved, mostly just around the tradeoff of additional complexity vs perf increase (that is, where the additional complexity isn't worth the 1-2 instruction or 0.1-0.2 nanosecond savings, so using a more readable/maintainable/portable implementation is preferred).

@tannergoodingtannergooding added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

Sorry I was quite busy the last couple of weeks.
I think that except for Vector3.Transform they are all resolved, just not marked as resolved.

@dotnet-policy-servicedotnet-policy-serviceBot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenfmartenf closed this Jun 13, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 14, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Numericscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@martenf@tannergooding
, '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

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations - #99547

Closed
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd
Closed

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations#99547
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd

Conversation

@martenf

@martenfmartenf commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

Added SIMD path using Vector128 (and Vector256 for Matrix4x4) for:

Vector3.Cross(Vector3 vector1, Vector3 vector2)

MethodMeanErrorStdDevRatioRatioSD
Before1.3289 ns0.0531 ns0.0708 ns1.000.00
After0.6613 ns0.0399 ns0.0833 ns0.510.05

Vector3.Transform(Vector3 value, Quaternion rotation)

MethodMeanErrorStdDevRatioRatioSD
Before3.922 ns0.1034 ns0.1380 ns1.000.00
After1.258 ns0.0508 ns0.0776 ns0.320.02

Quaternion operator *(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
dotnet84.0531 ns0.1008 ns0.0990 ns1.000.00
dotnet90.7203 ns0.0312 ns0.0292 ns0.180.01
after0.5752 ns0.0185 ns0.0173 ns0.140.00

Quaternion operator /(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before6.010 ns0.1399 ns0.1240 ns1.000.00
after2.377 ns0.0334 ns0.0296 ns0.400.01

Quaternion Concatenate(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before4.166 ns0.0344 ns0.0321 ns0.690.02
after1.266 ns0.0443 ns0.0414 ns0.210.01

Impl operator *(in Impl left, in Impl right)

MethodMeanErrorStdDevRatio
before8.009 ns0.0856 ns0.0801 ns1.00
afterVector1285.407 ns0.0604 ns0.0565 ns0.68
afterVector2563.142 ns0.0616 ns0.0546 ns0.39

Vector3.Cross(Vector3 vector1, Vector3 vector2)
Vector3.Transform(Vector3 value, Quaternion rotation)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

added simd paths for quaternion multiplication, division and concatenate
added simd path for matrix4x4 multiplication
@martenfmartenf changed the title Added SIMD path for Vector3.Cross and Vector3.TransformAdded SIMD path for Vector3, Quaternion and Matrix4x4 operationsMar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet/area-system-numerics

Comment on lines +531 to +546
if (Vector128.IsHardwareAccelerated)
{
var vVector = value.AsVector128();
var rVector = Unsafe.BitCast<Quaternion, Vector128<float>>(rotation);

// v + 2 * (q x (q.W * v + q x v)
return (vVector + Vector128.Create(2f) * Cross(rVector, Vector128.Shuffle(rVector, Vector128.Create(3, 3, 3, 3)) * vVector + Cross(rVector, vVector))).AsVector3();

static Vector128<float> Cross(Vector128<float> v1, Vector128<float> v2)
{
return (Vector128.Shuffle(v1, Vector128.Create(1, 2, 0, 3)) *
Vector128.Shuffle(v2, Vector128.Create(2, 0, 1, 3))) -
(Vector128.Shuffle(v1, Vector128.Create(2, 0, 1, 3)) *
Vector128.Shuffle(v2, Vector128.Create(1, 2, 0, 3)));
}
}

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.

I'm not immediately following where this implementation is from...

The standard naive algorithm is typically broken down to something like:

return((Quaternion.Conjugate(rotation)*newQuaternion(value,0.0f))*rotation).AsVector128().AsVector3();

(noting that Quaternion.operator *(Quaternion, Quaterion) is not currently marked as aggressively inline)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I got it from a private C library I wrote a decade ago which quotes a dead link.
The closest I could find on the internet after a quick search is this forum post which gets 95% of the way there and just misses the last 'simplification for readability' step.

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.

Can we update this to follow the "standard" algorithm I gave above.

I expect we'll get better support for things like constant folding, more consistent performance across a range of hardware, and we can accurately quote the root implementation to a known source (which avoids any potential licensing issues): https://github.com/microsoft/DirectXMath/blob/main/Inc/DirectXMathVector.inl#L10307-L10317

IIRC, glm uses a similar algorithm for it's implementation

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication. It will also always be slower then the reduced version because it contains significantly more math operation.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

To reduce any variation between different archs the implementation could be changed to

return (vVector + 2 * Cross(rVector, Vector128.Create(rotation.W) * vVector + Cross(rVector, vVector))).AsVector3();

which would give the JIT a bit more leeway, but I doubt it makes any differens.

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.

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication

This is in part because quaternion multiplication is not being inlined currently. But, also because it's doing some "unnecessary" work, as you indicated. But that can be accounted for as well.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

In general we require correct attribution to exist and based on the statement given above, the code (even if you authored it) was itself based on some internet blog/forum/site/etc post that can no longer be referenced (due to a dead link). Such a post may have itself may have been based on something else which overall makes it very difficult for us to use "safely" as the chain of citations is broken.

The actual requirements for attribution, whether or not something can be copyrighted, and the like can get quite complex. When there isn't a clear chain anymore, then it requires additional effort on our part, potentially even requiring checks with legal, so an actual lawyer can make the determination on whether or not it is safe to do.

.NET is a huge open source project depending on by millions. We must do the due diligence and ensure that all the boxes are checked.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

What makes the result of Q*vQ any more correct that any of the other ways you could use to rotate a vector by the rotation saved in a quaternion? Rodrigues' formula is actually older than Hamilton's, but it's not like any of this is a universal law written in the stars. This entire conversation feels pointless so I will stop wasting everyone's time, close this pull request and just use a private library.

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.

From a mathematical perspective they all produce the "same" result. The consideration is that they are not the same from a programmatic perspective due to additional considerations.

The intent was to push this PR in a direction where we could still improve the performance but without changing certain invariants that are extremely important to maintain. That requires more in depth consideration and documentation to show how those requirements are being met.

The intent was not to waste time or get into an argument over what is right vs wrong. The contribution was appreciated and was otherwise nearly in the right shape to be taken.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I know and appreciate that you weren't trying to waste any time, but I just don't have the spare time to have long debates about computational vs mathematical correctness right now, so I think it's just easier if I leave it at that so that someone else can make a pull request if they run into the same bottlenecks.

@tannergoodingtannergoodingJun 14, 2024

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.

👍, are you fine with me cherry-picking your commits into a PR so I can fix the last couple bits of feedback and get merged? That way you can still get credit for the overall contribution

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment on lines +163 to +178
var l0012 = Vector128.Shuffle(left, Vector128.Create(0, 0, 1, 2));
var l1120 = Vector128.Shuffle(left, Vector128.Create(1, 1, 2, 0));
var r0333 = Vector128.Shuffle(right, Vector128.Create(0, 3, 3, 3));
var r1201 = Vector128.Shuffle(right, Vector128.Create(1, 2, 0, 1));

var t0 = l0012 * r0333 + l1120 * r1201;
var mask = Vector128.Create(0x80000000, 0, 0, 0);
var t0m = Vector128.Xor(t0, mask.AsSingle());

var l2201 = Vector128.Shuffle(left, Vector128.Create(2, 2, 0, 1));
var l3333 = Vector128.Shuffle(left, Vector128.Create(3, 3, 3, 3));
var r2120 = Vector128.Shuffle(right, Vector128.Create(2, 1, 2, 0));
var r3012 = Vector128.Shuffle(right, Vector128.Create(3, 0, 1, 2));

var t1 = l3333 * r3012 - l2201 * r2120 + t0m;
var result = Vector128.Shuffle(t1, Vector128.Create(1, 2, 3, 0));

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.

This new code is much harder to read for basically a cycle difference in perf (numbers above report 0.7 -> 0.5 ns). It may also not optimize as well when considering all platforms or ISA baselines (SSE2, SSE4.1, AVX2, AVX512F, AdvSimd, WASM, etc).

It's also substantially different from Concatenate despite them doing the "same thing" (just one is x * y and the other is y * x)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's an operation that tends to be in the hot path for 3d applications so thought the reduced readability might be worth the extra performance on all the platforms I tested it on.

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.

Its not that hot for 3D apps. The actual hot code tends to happen on the GPU instead.

Its especially not so hot that the complexity is worth 0.15ns (which is effectively 1 CPU cycle).

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.

This comment is still pending. I don't think the new code is worth the 0.15ns savings (it's notably slightly slower on my box too). The code is more complex, harder to follow, and so I don't think the justification is there to take this part of the change.

.NET is huge, it is millions of lines of code. We ultimately have to balance readability, maintainability, security, perf, etc.

While Vector3 is itself a performance oriented type, we still have to factor in the other considerations and balance them out. It also applies beyond what a simple benchmark may measure, as we have multiple platforms, ISAs, and ABIs to consider. We also have to consider how certain optimizations may function (such as inlining and constant folding).

As such, even with it being a performance oriented type, there's many places where we are happy to give up a half nanosecond (which is in practice 1-5 instruction cycles) to improve the other areas.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
@tannergooding

Copy link
Copy Markdown
Member

@martenf is this something you're still working on? There's still some pending feedback above that needs to be resolved, mostly just around the tradeoff of additional complexity vs perf increase (that is, where the additional complexity isn't worth the 1-2 instruction or 0.1-0.2 nanosecond savings, so using a more readable/maintainable/portable implementation is preferred).

@tannergoodingtannergooding added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

Sorry I was quite busy the last couple of weeks.
I think that except for Vector3.Transform they are all resolved, just not marked as resolved.

@dotnet-policy-servicedotnet-policy-serviceBot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenfmartenf closed this Jun 13, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 14, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Numericscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@martenf@tannergooding
, '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

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations - #99547

Closed
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd
Closed

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations#99547
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd

Conversation

@martenf

@martenfmartenf commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

Added SIMD path using Vector128 (and Vector256 for Matrix4x4) for:

Vector3.Cross(Vector3 vector1, Vector3 vector2)

MethodMeanErrorStdDevRatioRatioSD
Before1.3289 ns0.0531 ns0.0708 ns1.000.00
After0.6613 ns0.0399 ns0.0833 ns0.510.05

Vector3.Transform(Vector3 value, Quaternion rotation)

MethodMeanErrorStdDevRatioRatioSD
Before3.922 ns0.1034 ns0.1380 ns1.000.00
After1.258 ns0.0508 ns0.0776 ns0.320.02

Quaternion operator *(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
dotnet84.0531 ns0.1008 ns0.0990 ns1.000.00
dotnet90.7203 ns0.0312 ns0.0292 ns0.180.01
after0.5752 ns0.0185 ns0.0173 ns0.140.00

Quaternion operator /(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before6.010 ns0.1399 ns0.1240 ns1.000.00
after2.377 ns0.0334 ns0.0296 ns0.400.01

Quaternion Concatenate(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before4.166 ns0.0344 ns0.0321 ns0.690.02
after1.266 ns0.0443 ns0.0414 ns0.210.01

Impl operator *(in Impl left, in Impl right)

MethodMeanErrorStdDevRatio
before8.009 ns0.0856 ns0.0801 ns1.00
afterVector1285.407 ns0.0604 ns0.0565 ns0.68
afterVector2563.142 ns0.0616 ns0.0546 ns0.39

Vector3.Cross(Vector3 vector1, Vector3 vector2)
Vector3.Transform(Vector3 value, Quaternion rotation)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

added simd paths for quaternion multiplication, division and concatenate
added simd path for matrix4x4 multiplication
@martenfmartenf changed the title Added SIMD path for Vector3.Cross and Vector3.TransformAdded SIMD path for Vector3, Quaternion and Matrix4x4 operationsMar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet/area-system-numerics

Comment on lines +531 to +546
if (Vector128.IsHardwareAccelerated)
{
var vVector = value.AsVector128();
var rVector = Unsafe.BitCast<Quaternion, Vector128<float>>(rotation);

// v + 2 * (q x (q.W * v + q x v)
return (vVector + Vector128.Create(2f) * Cross(rVector, Vector128.Shuffle(rVector, Vector128.Create(3, 3, 3, 3)) * vVector + Cross(rVector, vVector))).AsVector3();

static Vector128<float> Cross(Vector128<float> v1, Vector128<float> v2)
{
return (Vector128.Shuffle(v1, Vector128.Create(1, 2, 0, 3)) *
Vector128.Shuffle(v2, Vector128.Create(2, 0, 1, 3))) -
(Vector128.Shuffle(v1, Vector128.Create(2, 0, 1, 3)) *
Vector128.Shuffle(v2, Vector128.Create(1, 2, 0, 3)));
}
}

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.

I'm not immediately following where this implementation is from...

The standard naive algorithm is typically broken down to something like:

return((Quaternion.Conjugate(rotation)*newQuaternion(value,0.0f))*rotation).AsVector128().AsVector3();

(noting that Quaternion.operator *(Quaternion, Quaterion) is not currently marked as aggressively inline)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I got it from a private C library I wrote a decade ago which quotes a dead link.
The closest I could find on the internet after a quick search is this forum post which gets 95% of the way there and just misses the last 'simplification for readability' step.

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.

Can we update this to follow the "standard" algorithm I gave above.

I expect we'll get better support for things like constant folding, more consistent performance across a range of hardware, and we can accurately quote the root implementation to a known source (which avoids any potential licensing issues): https://github.com/microsoft/DirectXMath/blob/main/Inc/DirectXMathVector.inl#L10307-L10317

IIRC, glm uses a similar algorithm for it's implementation

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication. It will also always be slower then the reduced version because it contains significantly more math operation.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

To reduce any variation between different archs the implementation could be changed to

return (vVector + 2 * Cross(rVector, Vector128.Create(rotation.W) * vVector + Cross(rVector, vVector))).AsVector3();

which would give the JIT a bit more leeway, but I doubt it makes any differens.

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.

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication

This is in part because quaternion multiplication is not being inlined currently. But, also because it's doing some "unnecessary" work, as you indicated. But that can be accounted for as well.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

In general we require correct attribution to exist and based on the statement given above, the code (even if you authored it) was itself based on some internet blog/forum/site/etc post that can no longer be referenced (due to a dead link). Such a post may have itself may have been based on something else which overall makes it very difficult for us to use "safely" as the chain of citations is broken.

The actual requirements for attribution, whether or not something can be copyrighted, and the like can get quite complex. When there isn't a clear chain anymore, then it requires additional effort on our part, potentially even requiring checks with legal, so an actual lawyer can make the determination on whether or not it is safe to do.

.NET is a huge open source project depending on by millions. We must do the due diligence and ensure that all the boxes are checked.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

What makes the result of Q*vQ any more correct that any of the other ways you could use to rotate a vector by the rotation saved in a quaternion? Rodrigues' formula is actually older than Hamilton's, but it's not like any of this is a universal law written in the stars. This entire conversation feels pointless so I will stop wasting everyone's time, close this pull request and just use a private library.

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.

From a mathematical perspective they all produce the "same" result. The consideration is that they are not the same from a programmatic perspective due to additional considerations.

The intent was to push this PR in a direction where we could still improve the performance but without changing certain invariants that are extremely important to maintain. That requires more in depth consideration and documentation to show how those requirements are being met.

The intent was not to waste time or get into an argument over what is right vs wrong. The contribution was appreciated and was otherwise nearly in the right shape to be taken.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I know and appreciate that you weren't trying to waste any time, but I just don't have the spare time to have long debates about computational vs mathematical correctness right now, so I think it's just easier if I leave it at that so that someone else can make a pull request if they run into the same bottlenecks.

@tannergoodingtannergoodingJun 14, 2024

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.

👍, are you fine with me cherry-picking your commits into a PR so I can fix the last couple bits of feedback and get merged? That way you can still get credit for the overall contribution

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment on lines +163 to +178
var l0012 = Vector128.Shuffle(left, Vector128.Create(0, 0, 1, 2));
var l1120 = Vector128.Shuffle(left, Vector128.Create(1, 1, 2, 0));
var r0333 = Vector128.Shuffle(right, Vector128.Create(0, 3, 3, 3));
var r1201 = Vector128.Shuffle(right, Vector128.Create(1, 2, 0, 1));

var t0 = l0012 * r0333 + l1120 * r1201;
var mask = Vector128.Create(0x80000000, 0, 0, 0);
var t0m = Vector128.Xor(t0, mask.AsSingle());

var l2201 = Vector128.Shuffle(left, Vector128.Create(2, 2, 0, 1));
var l3333 = Vector128.Shuffle(left, Vector128.Create(3, 3, 3, 3));
var r2120 = Vector128.Shuffle(right, Vector128.Create(2, 1, 2, 0));
var r3012 = Vector128.Shuffle(right, Vector128.Create(3, 0, 1, 2));

var t1 = l3333 * r3012 - l2201 * r2120 + t0m;
var result = Vector128.Shuffle(t1, Vector128.Create(1, 2, 3, 0));

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.

This new code is much harder to read for basically a cycle difference in perf (numbers above report 0.7 -> 0.5 ns). It may also not optimize as well when considering all platforms or ISA baselines (SSE2, SSE4.1, AVX2, AVX512F, AdvSimd, WASM, etc).

It's also substantially different from Concatenate despite them doing the "same thing" (just one is x * y and the other is y * x)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's an operation that tends to be in the hot path for 3d applications so thought the reduced readability might be worth the extra performance on all the platforms I tested it on.

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.

Its not that hot for 3D apps. The actual hot code tends to happen on the GPU instead.

Its especially not so hot that the complexity is worth 0.15ns (which is effectively 1 CPU cycle).

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.

This comment is still pending. I don't think the new code is worth the 0.15ns savings (it's notably slightly slower on my box too). The code is more complex, harder to follow, and so I don't think the justification is there to take this part of the change.

.NET is huge, it is millions of lines of code. We ultimately have to balance readability, maintainability, security, perf, etc.

While Vector3 is itself a performance oriented type, we still have to factor in the other considerations and balance them out. It also applies beyond what a simple benchmark may measure, as we have multiple platforms, ISAs, and ABIs to consider. We also have to consider how certain optimizations may function (such as inlining and constant folding).

As such, even with it being a performance oriented type, there's many places where we are happy to give up a half nanosecond (which is in practice 1-5 instruction cycles) to improve the other areas.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
@tannergooding

Copy link
Copy Markdown
Member

@martenf is this something you're still working on? There's still some pending feedback above that needs to be resolved, mostly just around the tradeoff of additional complexity vs perf increase (that is, where the additional complexity isn't worth the 1-2 instruction or 0.1-0.2 nanosecond savings, so using a more readable/maintainable/portable implementation is preferred).

@tannergoodingtannergooding added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

Sorry I was quite busy the last couple of weeks.
I think that except for Vector3.Transform they are all resolved, just not marked as resolved.

@dotnet-policy-servicedotnet-policy-serviceBot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenfmartenf closed this Jun 13, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 14, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Numericscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@martenf@tannergooding
, '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

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations - #99547

Closed
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd
Closed

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations#99547
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd

Conversation

@martenf

@martenfmartenf commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

Added SIMD path using Vector128 (and Vector256 for Matrix4x4) for:

Vector3.Cross(Vector3 vector1, Vector3 vector2)

MethodMeanErrorStdDevRatioRatioSD
Before1.3289 ns0.0531 ns0.0708 ns1.000.00
After0.6613 ns0.0399 ns0.0833 ns0.510.05

Vector3.Transform(Vector3 value, Quaternion rotation)

MethodMeanErrorStdDevRatioRatioSD
Before3.922 ns0.1034 ns0.1380 ns1.000.00
After1.258 ns0.0508 ns0.0776 ns0.320.02

Quaternion operator *(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
dotnet84.0531 ns0.1008 ns0.0990 ns1.000.00
dotnet90.7203 ns0.0312 ns0.0292 ns0.180.01
after0.5752 ns0.0185 ns0.0173 ns0.140.00

Quaternion operator /(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before6.010 ns0.1399 ns0.1240 ns1.000.00
after2.377 ns0.0334 ns0.0296 ns0.400.01

Quaternion Concatenate(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before4.166 ns0.0344 ns0.0321 ns0.690.02
after1.266 ns0.0443 ns0.0414 ns0.210.01

Impl operator *(in Impl left, in Impl right)

MethodMeanErrorStdDevRatio
before8.009 ns0.0856 ns0.0801 ns1.00
afterVector1285.407 ns0.0604 ns0.0565 ns0.68
afterVector2563.142 ns0.0616 ns0.0546 ns0.39

Vector3.Cross(Vector3 vector1, Vector3 vector2)
Vector3.Transform(Vector3 value, Quaternion rotation)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

added simd paths for quaternion multiplication, division and concatenate
added simd path for matrix4x4 multiplication
@martenfmartenf changed the title Added SIMD path for Vector3.Cross and Vector3.TransformAdded SIMD path for Vector3, Quaternion and Matrix4x4 operationsMar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet/area-system-numerics

Comment on lines +531 to +546
if (Vector128.IsHardwareAccelerated)
{
var vVector = value.AsVector128();
var rVector = Unsafe.BitCast<Quaternion, Vector128<float>>(rotation);

// v + 2 * (q x (q.W * v + q x v)
return (vVector + Vector128.Create(2f) * Cross(rVector, Vector128.Shuffle(rVector, Vector128.Create(3, 3, 3, 3)) * vVector + Cross(rVector, vVector))).AsVector3();

static Vector128<float> Cross(Vector128<float> v1, Vector128<float> v2)
{
return (Vector128.Shuffle(v1, Vector128.Create(1, 2, 0, 3)) *
Vector128.Shuffle(v2, Vector128.Create(2, 0, 1, 3))) -
(Vector128.Shuffle(v1, Vector128.Create(2, 0, 1, 3)) *
Vector128.Shuffle(v2, Vector128.Create(1, 2, 0, 3)));
}
}

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.

I'm not immediately following where this implementation is from...

The standard naive algorithm is typically broken down to something like:

return((Quaternion.Conjugate(rotation)*newQuaternion(value,0.0f))*rotation).AsVector128().AsVector3();

(noting that Quaternion.operator *(Quaternion, Quaterion) is not currently marked as aggressively inline)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I got it from a private C library I wrote a decade ago which quotes a dead link.
The closest I could find on the internet after a quick search is this forum post which gets 95% of the way there and just misses the last 'simplification for readability' step.

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.

Can we update this to follow the "standard" algorithm I gave above.

I expect we'll get better support for things like constant folding, more consistent performance across a range of hardware, and we can accurately quote the root implementation to a known source (which avoids any potential licensing issues): https://github.com/microsoft/DirectXMath/blob/main/Inc/DirectXMathVector.inl#L10307-L10317

IIRC, glm uses a similar algorithm for it's implementation

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication. It will also always be slower then the reduced version because it contains significantly more math operation.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

To reduce any variation between different archs the implementation could be changed to

return (vVector + 2 * Cross(rVector, Vector128.Create(rotation.W) * vVector + Cross(rVector, vVector))).AsVector3();

which would give the JIT a bit more leeway, but I doubt it makes any differens.

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.

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication

This is in part because quaternion multiplication is not being inlined currently. But, also because it's doing some "unnecessary" work, as you indicated. But that can be accounted for as well.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

In general we require correct attribution to exist and based on the statement given above, the code (even if you authored it) was itself based on some internet blog/forum/site/etc post that can no longer be referenced (due to a dead link). Such a post may have itself may have been based on something else which overall makes it very difficult for us to use "safely" as the chain of citations is broken.

The actual requirements for attribution, whether or not something can be copyrighted, and the like can get quite complex. When there isn't a clear chain anymore, then it requires additional effort on our part, potentially even requiring checks with legal, so an actual lawyer can make the determination on whether or not it is safe to do.

.NET is a huge open source project depending on by millions. We must do the due diligence and ensure that all the boxes are checked.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

What makes the result of Q*vQ any more correct that any of the other ways you could use to rotate a vector by the rotation saved in a quaternion? Rodrigues' formula is actually older than Hamilton's, but it's not like any of this is a universal law written in the stars. This entire conversation feels pointless so I will stop wasting everyone's time, close this pull request and just use a private library.

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.

From a mathematical perspective they all produce the "same" result. The consideration is that they are not the same from a programmatic perspective due to additional considerations.

The intent was to push this PR in a direction where we could still improve the performance but without changing certain invariants that are extremely important to maintain. That requires more in depth consideration and documentation to show how those requirements are being met.

The intent was not to waste time or get into an argument over what is right vs wrong. The contribution was appreciated and was otherwise nearly in the right shape to be taken.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I know and appreciate that you weren't trying to waste any time, but I just don't have the spare time to have long debates about computational vs mathematical correctness right now, so I think it's just easier if I leave it at that so that someone else can make a pull request if they run into the same bottlenecks.

@tannergoodingtannergoodingJun 14, 2024

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.

👍, are you fine with me cherry-picking your commits into a PR so I can fix the last couple bits of feedback and get merged? That way you can still get credit for the overall contribution

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment on lines +163 to +178
var l0012 = Vector128.Shuffle(left, Vector128.Create(0, 0, 1, 2));
var l1120 = Vector128.Shuffle(left, Vector128.Create(1, 1, 2, 0));
var r0333 = Vector128.Shuffle(right, Vector128.Create(0, 3, 3, 3));
var r1201 = Vector128.Shuffle(right, Vector128.Create(1, 2, 0, 1));

var t0 = l0012 * r0333 + l1120 * r1201;
var mask = Vector128.Create(0x80000000, 0, 0, 0);
var t0m = Vector128.Xor(t0, mask.AsSingle());

var l2201 = Vector128.Shuffle(left, Vector128.Create(2, 2, 0, 1));
var l3333 = Vector128.Shuffle(left, Vector128.Create(3, 3, 3, 3));
var r2120 = Vector128.Shuffle(right, Vector128.Create(2, 1, 2, 0));
var r3012 = Vector128.Shuffle(right, Vector128.Create(3, 0, 1, 2));

var t1 = l3333 * r3012 - l2201 * r2120 + t0m;
var result = Vector128.Shuffle(t1, Vector128.Create(1, 2, 3, 0));

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.

This new code is much harder to read for basically a cycle difference in perf (numbers above report 0.7 -> 0.5 ns). It may also not optimize as well when considering all platforms or ISA baselines (SSE2, SSE4.1, AVX2, AVX512F, AdvSimd, WASM, etc).

It's also substantially different from Concatenate despite them doing the "same thing" (just one is x * y and the other is y * x)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's an operation that tends to be in the hot path for 3d applications so thought the reduced readability might be worth the extra performance on all the platforms I tested it on.

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.

Its not that hot for 3D apps. The actual hot code tends to happen on the GPU instead.

Its especially not so hot that the complexity is worth 0.15ns (which is effectively 1 CPU cycle).

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.

This comment is still pending. I don't think the new code is worth the 0.15ns savings (it's notably slightly slower on my box too). The code is more complex, harder to follow, and so I don't think the justification is there to take this part of the change.

.NET is huge, it is millions of lines of code. We ultimately have to balance readability, maintainability, security, perf, etc.

While Vector3 is itself a performance oriented type, we still have to factor in the other considerations and balance them out. It also applies beyond what a simple benchmark may measure, as we have multiple platforms, ISAs, and ABIs to consider. We also have to consider how certain optimizations may function (such as inlining and constant folding).

As such, even with it being a performance oriented type, there's many places where we are happy to give up a half nanosecond (which is in practice 1-5 instruction cycles) to improve the other areas.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
@tannergooding

Copy link
Copy Markdown
Member

@martenf is this something you're still working on? There's still some pending feedback above that needs to be resolved, mostly just around the tradeoff of additional complexity vs perf increase (that is, where the additional complexity isn't worth the 1-2 instruction or 0.1-0.2 nanosecond savings, so using a more readable/maintainable/portable implementation is preferred).

@tannergoodingtannergooding added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

Sorry I was quite busy the last couple of weeks.
I think that except for Vector3.Transform they are all resolved, just not marked as resolved.

@dotnet-policy-servicedotnet-policy-serviceBot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenfmartenf closed this Jun 13, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 14, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Numericscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@martenf@tannergooding
, '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

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations - #99547

Closed
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd
Closed

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations#99547
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd

Conversation

@martenf

@martenfmartenf commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

Added SIMD path using Vector128 (and Vector256 for Matrix4x4) for:

Vector3.Cross(Vector3 vector1, Vector3 vector2)

MethodMeanErrorStdDevRatioRatioSD
Before1.3289 ns0.0531 ns0.0708 ns1.000.00
After0.6613 ns0.0399 ns0.0833 ns0.510.05

Vector3.Transform(Vector3 value, Quaternion rotation)

MethodMeanErrorStdDevRatioRatioSD
Before3.922 ns0.1034 ns0.1380 ns1.000.00
After1.258 ns0.0508 ns0.0776 ns0.320.02

Quaternion operator *(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
dotnet84.0531 ns0.1008 ns0.0990 ns1.000.00
dotnet90.7203 ns0.0312 ns0.0292 ns0.180.01
after0.5752 ns0.0185 ns0.0173 ns0.140.00

Quaternion operator /(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before6.010 ns0.1399 ns0.1240 ns1.000.00
after2.377 ns0.0334 ns0.0296 ns0.400.01

Quaternion Concatenate(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before4.166 ns0.0344 ns0.0321 ns0.690.02
after1.266 ns0.0443 ns0.0414 ns0.210.01

Impl operator *(in Impl left, in Impl right)

MethodMeanErrorStdDevRatio
before8.009 ns0.0856 ns0.0801 ns1.00
afterVector1285.407 ns0.0604 ns0.0565 ns0.68
afterVector2563.142 ns0.0616 ns0.0546 ns0.39

Vector3.Cross(Vector3 vector1, Vector3 vector2)
Vector3.Transform(Vector3 value, Quaternion rotation)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

added simd paths for quaternion multiplication, division and concatenate
added simd path for matrix4x4 multiplication
@martenfmartenf changed the title Added SIMD path for Vector3.Cross and Vector3.TransformAdded SIMD path for Vector3, Quaternion and Matrix4x4 operationsMar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet/area-system-numerics

Comment on lines +531 to +546
if (Vector128.IsHardwareAccelerated)
{
var vVector = value.AsVector128();
var rVector = Unsafe.BitCast<Quaternion, Vector128<float>>(rotation);

// v + 2 * (q x (q.W * v + q x v)
return (vVector + Vector128.Create(2f) * Cross(rVector, Vector128.Shuffle(rVector, Vector128.Create(3, 3, 3, 3)) * vVector + Cross(rVector, vVector))).AsVector3();

static Vector128<float> Cross(Vector128<float> v1, Vector128<float> v2)
{
return (Vector128.Shuffle(v1, Vector128.Create(1, 2, 0, 3)) *
Vector128.Shuffle(v2, Vector128.Create(2, 0, 1, 3))) -
(Vector128.Shuffle(v1, Vector128.Create(2, 0, 1, 3)) *
Vector128.Shuffle(v2, Vector128.Create(1, 2, 0, 3)));
}
}

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.

I'm not immediately following where this implementation is from...

The standard naive algorithm is typically broken down to something like:

return((Quaternion.Conjugate(rotation)*newQuaternion(value,0.0f))*rotation).AsVector128().AsVector3();

(noting that Quaternion.operator *(Quaternion, Quaterion) is not currently marked as aggressively inline)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I got it from a private C library I wrote a decade ago which quotes a dead link.
The closest I could find on the internet after a quick search is this forum post which gets 95% of the way there and just misses the last 'simplification for readability' step.

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.

Can we update this to follow the "standard" algorithm I gave above.

I expect we'll get better support for things like constant folding, more consistent performance across a range of hardware, and we can accurately quote the root implementation to a known source (which avoids any potential licensing issues): https://github.com/microsoft/DirectXMath/blob/main/Inc/DirectXMathVector.inl#L10307-L10317

IIRC, glm uses a similar algorithm for it's implementation

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication. It will also always be slower then the reduced version because it contains significantly more math operation.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

To reduce any variation between different archs the implementation could be changed to

return (vVector + 2 * Cross(rVector, Vector128.Create(rotation.W) * vVector + Cross(rVector, vVector))).AsVector3();

which would give the JIT a bit more leeway, but I doubt it makes any differens.

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.

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication

This is in part because quaternion multiplication is not being inlined currently. But, also because it's doing some "unnecessary" work, as you indicated. But that can be accounted for as well.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

In general we require correct attribution to exist and based on the statement given above, the code (even if you authored it) was itself based on some internet blog/forum/site/etc post that can no longer be referenced (due to a dead link). Such a post may have itself may have been based on something else which overall makes it very difficult for us to use "safely" as the chain of citations is broken.

The actual requirements for attribution, whether or not something can be copyrighted, and the like can get quite complex. When there isn't a clear chain anymore, then it requires additional effort on our part, potentially even requiring checks with legal, so an actual lawyer can make the determination on whether or not it is safe to do.

.NET is a huge open source project depending on by millions. We must do the due diligence and ensure that all the boxes are checked.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

What makes the result of Q*vQ any more correct that any of the other ways you could use to rotate a vector by the rotation saved in a quaternion? Rodrigues' formula is actually older than Hamilton's, but it's not like any of this is a universal law written in the stars. This entire conversation feels pointless so I will stop wasting everyone's time, close this pull request and just use a private library.

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.

From a mathematical perspective they all produce the "same" result. The consideration is that they are not the same from a programmatic perspective due to additional considerations.

The intent was to push this PR in a direction where we could still improve the performance but without changing certain invariants that are extremely important to maintain. That requires more in depth consideration and documentation to show how those requirements are being met.

The intent was not to waste time or get into an argument over what is right vs wrong. The contribution was appreciated and was otherwise nearly in the right shape to be taken.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I know and appreciate that you weren't trying to waste any time, but I just don't have the spare time to have long debates about computational vs mathematical correctness right now, so I think it's just easier if I leave it at that so that someone else can make a pull request if they run into the same bottlenecks.

@tannergoodingtannergoodingJun 14, 2024

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.

👍, are you fine with me cherry-picking your commits into a PR so I can fix the last couple bits of feedback and get merged? That way you can still get credit for the overall contribution

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment on lines +163 to +178
var l0012 = Vector128.Shuffle(left, Vector128.Create(0, 0, 1, 2));
var l1120 = Vector128.Shuffle(left, Vector128.Create(1, 1, 2, 0));
var r0333 = Vector128.Shuffle(right, Vector128.Create(0, 3, 3, 3));
var r1201 = Vector128.Shuffle(right, Vector128.Create(1, 2, 0, 1));

var t0 = l0012 * r0333 + l1120 * r1201;
var mask = Vector128.Create(0x80000000, 0, 0, 0);
var t0m = Vector128.Xor(t0, mask.AsSingle());

var l2201 = Vector128.Shuffle(left, Vector128.Create(2, 2, 0, 1));
var l3333 = Vector128.Shuffle(left, Vector128.Create(3, 3, 3, 3));
var r2120 = Vector128.Shuffle(right, Vector128.Create(2, 1, 2, 0));
var r3012 = Vector128.Shuffle(right, Vector128.Create(3, 0, 1, 2));

var t1 = l3333 * r3012 - l2201 * r2120 + t0m;
var result = Vector128.Shuffle(t1, Vector128.Create(1, 2, 3, 0));

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.

This new code is much harder to read for basically a cycle difference in perf (numbers above report 0.7 -> 0.5 ns). It may also not optimize as well when considering all platforms or ISA baselines (SSE2, SSE4.1, AVX2, AVX512F, AdvSimd, WASM, etc).

It's also substantially different from Concatenate despite them doing the "same thing" (just one is x * y and the other is y * x)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's an operation that tends to be in the hot path for 3d applications so thought the reduced readability might be worth the extra performance on all the platforms I tested it on.

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.

Its not that hot for 3D apps. The actual hot code tends to happen on the GPU instead.

Its especially not so hot that the complexity is worth 0.15ns (which is effectively 1 CPU cycle).

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.

This comment is still pending. I don't think the new code is worth the 0.15ns savings (it's notably slightly slower on my box too). The code is more complex, harder to follow, and so I don't think the justification is there to take this part of the change.

.NET is huge, it is millions of lines of code. We ultimately have to balance readability, maintainability, security, perf, etc.

While Vector3 is itself a performance oriented type, we still have to factor in the other considerations and balance them out. It also applies beyond what a simple benchmark may measure, as we have multiple platforms, ISAs, and ABIs to consider. We also have to consider how certain optimizations may function (such as inlining and constant folding).

As such, even with it being a performance oriented type, there's many places where we are happy to give up a half nanosecond (which is in practice 1-5 instruction cycles) to improve the other areas.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
@tannergooding

Copy link
Copy Markdown
Member

@martenf is this something you're still working on? There's still some pending feedback above that needs to be resolved, mostly just around the tradeoff of additional complexity vs perf increase (that is, where the additional complexity isn't worth the 1-2 instruction or 0.1-0.2 nanosecond savings, so using a more readable/maintainable/portable implementation is preferred).

@tannergoodingtannergooding added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

Sorry I was quite busy the last couple of weeks.
I think that except for Vector3.Transform they are all resolved, just not marked as resolved.

@dotnet-policy-servicedotnet-policy-serviceBot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenfmartenf closed this Jun 13, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 14, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Numericscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@martenf@tannergooding
, '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

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations - #99547

Closed
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd
Closed

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations#99547
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd

Conversation

@martenf

@martenfmartenf commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

Added SIMD path using Vector128 (and Vector256 for Matrix4x4) for:

Vector3.Cross(Vector3 vector1, Vector3 vector2)

MethodMeanErrorStdDevRatioRatioSD
Before1.3289 ns0.0531 ns0.0708 ns1.000.00
After0.6613 ns0.0399 ns0.0833 ns0.510.05

Vector3.Transform(Vector3 value, Quaternion rotation)

MethodMeanErrorStdDevRatioRatioSD
Before3.922 ns0.1034 ns0.1380 ns1.000.00
After1.258 ns0.0508 ns0.0776 ns0.320.02

Quaternion operator *(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
dotnet84.0531 ns0.1008 ns0.0990 ns1.000.00
dotnet90.7203 ns0.0312 ns0.0292 ns0.180.01
after0.5752 ns0.0185 ns0.0173 ns0.140.00

Quaternion operator /(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before6.010 ns0.1399 ns0.1240 ns1.000.00
after2.377 ns0.0334 ns0.0296 ns0.400.01

Quaternion Concatenate(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before4.166 ns0.0344 ns0.0321 ns0.690.02
after1.266 ns0.0443 ns0.0414 ns0.210.01

Impl operator *(in Impl left, in Impl right)

MethodMeanErrorStdDevRatio
before8.009 ns0.0856 ns0.0801 ns1.00
afterVector1285.407 ns0.0604 ns0.0565 ns0.68
afterVector2563.142 ns0.0616 ns0.0546 ns0.39

Vector3.Cross(Vector3 vector1, Vector3 vector2)
Vector3.Transform(Vector3 value, Quaternion rotation)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

added simd paths for quaternion multiplication, division and concatenate
added simd path for matrix4x4 multiplication
@martenfmartenf changed the title Added SIMD path for Vector3.Cross and Vector3.TransformAdded SIMD path for Vector3, Quaternion and Matrix4x4 operationsMar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet/area-system-numerics

Comment on lines +531 to +546
if (Vector128.IsHardwareAccelerated)
{
var vVector = value.AsVector128();
var rVector = Unsafe.BitCast<Quaternion, Vector128<float>>(rotation);

// v + 2 * (q x (q.W * v + q x v)
return (vVector + Vector128.Create(2f) * Cross(rVector, Vector128.Shuffle(rVector, Vector128.Create(3, 3, 3, 3)) * vVector + Cross(rVector, vVector))).AsVector3();

static Vector128<float> Cross(Vector128<float> v1, Vector128<float> v2)
{
return (Vector128.Shuffle(v1, Vector128.Create(1, 2, 0, 3)) *
Vector128.Shuffle(v2, Vector128.Create(2, 0, 1, 3))) -
(Vector128.Shuffle(v1, Vector128.Create(2, 0, 1, 3)) *
Vector128.Shuffle(v2, Vector128.Create(1, 2, 0, 3)));
}
}

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.

I'm not immediately following where this implementation is from...

The standard naive algorithm is typically broken down to something like:

return((Quaternion.Conjugate(rotation)*newQuaternion(value,0.0f))*rotation).AsVector128().AsVector3();

(noting that Quaternion.operator *(Quaternion, Quaterion) is not currently marked as aggressively inline)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I got it from a private C library I wrote a decade ago which quotes a dead link.
The closest I could find on the internet after a quick search is this forum post which gets 95% of the way there and just misses the last 'simplification for readability' step.

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.

Can we update this to follow the "standard" algorithm I gave above.

I expect we'll get better support for things like constant folding, more consistent performance across a range of hardware, and we can accurately quote the root implementation to a known source (which avoids any potential licensing issues): https://github.com/microsoft/DirectXMath/blob/main/Inc/DirectXMathVector.inl#L10307-L10317

IIRC, glm uses a similar algorithm for it's implementation

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication. It will also always be slower then the reduced version because it contains significantly more math operation.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

To reduce any variation between different archs the implementation could be changed to

return (vVector + 2 * Cross(rVector, Vector128.Create(rotation.W) * vVector + Cross(rVector, vVector))).AsVector3();

which would give the JIT a bit more leeway, but I doubt it makes any differens.

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.

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication

This is in part because quaternion multiplication is not being inlined currently. But, also because it's doing some "unnecessary" work, as you indicated. But that can be accounted for as well.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

In general we require correct attribution to exist and based on the statement given above, the code (even if you authored it) was itself based on some internet blog/forum/site/etc post that can no longer be referenced (due to a dead link). Such a post may have itself may have been based on something else which overall makes it very difficult for us to use "safely" as the chain of citations is broken.

The actual requirements for attribution, whether or not something can be copyrighted, and the like can get quite complex. When there isn't a clear chain anymore, then it requires additional effort on our part, potentially even requiring checks with legal, so an actual lawyer can make the determination on whether or not it is safe to do.

.NET is a huge open source project depending on by millions. We must do the due diligence and ensure that all the boxes are checked.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

What makes the result of Q*vQ any more correct that any of the other ways you could use to rotate a vector by the rotation saved in a quaternion? Rodrigues' formula is actually older than Hamilton's, but it's not like any of this is a universal law written in the stars. This entire conversation feels pointless so I will stop wasting everyone's time, close this pull request and just use a private library.

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.

From a mathematical perspective they all produce the "same" result. The consideration is that they are not the same from a programmatic perspective due to additional considerations.

The intent was to push this PR in a direction where we could still improve the performance but without changing certain invariants that are extremely important to maintain. That requires more in depth consideration and documentation to show how those requirements are being met.

The intent was not to waste time or get into an argument over what is right vs wrong. The contribution was appreciated and was otherwise nearly in the right shape to be taken.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I know and appreciate that you weren't trying to waste any time, but I just don't have the spare time to have long debates about computational vs mathematical correctness right now, so I think it's just easier if I leave it at that so that someone else can make a pull request if they run into the same bottlenecks.

@tannergoodingtannergoodingJun 14, 2024

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.

👍, are you fine with me cherry-picking your commits into a PR so I can fix the last couple bits of feedback and get merged? That way you can still get credit for the overall contribution

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment on lines +163 to +178
var l0012 = Vector128.Shuffle(left, Vector128.Create(0, 0, 1, 2));
var l1120 = Vector128.Shuffle(left, Vector128.Create(1, 1, 2, 0));
var r0333 = Vector128.Shuffle(right, Vector128.Create(0, 3, 3, 3));
var r1201 = Vector128.Shuffle(right, Vector128.Create(1, 2, 0, 1));

var t0 = l0012 * r0333 + l1120 * r1201;
var mask = Vector128.Create(0x80000000, 0, 0, 0);
var t0m = Vector128.Xor(t0, mask.AsSingle());

var l2201 = Vector128.Shuffle(left, Vector128.Create(2, 2, 0, 1));
var l3333 = Vector128.Shuffle(left, Vector128.Create(3, 3, 3, 3));
var r2120 = Vector128.Shuffle(right, Vector128.Create(2, 1, 2, 0));
var r3012 = Vector128.Shuffle(right, Vector128.Create(3, 0, 1, 2));

var t1 = l3333 * r3012 - l2201 * r2120 + t0m;
var result = Vector128.Shuffle(t1, Vector128.Create(1, 2, 3, 0));

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.

This new code is much harder to read for basically a cycle difference in perf (numbers above report 0.7 -> 0.5 ns). It may also not optimize as well when considering all platforms or ISA baselines (SSE2, SSE4.1, AVX2, AVX512F, AdvSimd, WASM, etc).

It's also substantially different from Concatenate despite them doing the "same thing" (just one is x * y and the other is y * x)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's an operation that tends to be in the hot path for 3d applications so thought the reduced readability might be worth the extra performance on all the platforms I tested it on.

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.

Its not that hot for 3D apps. The actual hot code tends to happen on the GPU instead.

Its especially not so hot that the complexity is worth 0.15ns (which is effectively 1 CPU cycle).

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.

This comment is still pending. I don't think the new code is worth the 0.15ns savings (it's notably slightly slower on my box too). The code is more complex, harder to follow, and so I don't think the justification is there to take this part of the change.

.NET is huge, it is millions of lines of code. We ultimately have to balance readability, maintainability, security, perf, etc.

While Vector3 is itself a performance oriented type, we still have to factor in the other considerations and balance them out. It also applies beyond what a simple benchmark may measure, as we have multiple platforms, ISAs, and ABIs to consider. We also have to consider how certain optimizations may function (such as inlining and constant folding).

As such, even with it being a performance oriented type, there's many places where we are happy to give up a half nanosecond (which is in practice 1-5 instruction cycles) to improve the other areas.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
@tannergooding

Copy link
Copy Markdown
Member

@martenf is this something you're still working on? There's still some pending feedback above that needs to be resolved, mostly just around the tradeoff of additional complexity vs perf increase (that is, where the additional complexity isn't worth the 1-2 instruction or 0.1-0.2 nanosecond savings, so using a more readable/maintainable/portable implementation is preferred).

@tannergoodingtannergooding added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

Sorry I was quite busy the last couple of weeks.
I think that except for Vector3.Transform they are all resolved, just not marked as resolved.

@dotnet-policy-servicedotnet-policy-serviceBot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenfmartenf closed this Jun 13, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 14, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Numericscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@martenf@tannergooding
, '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

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations - #99547

Closed
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd
Closed

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations#99547
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd

Conversation

@martenf

@martenfmartenf commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

Added SIMD path using Vector128 (and Vector256 for Matrix4x4) for:

Vector3.Cross(Vector3 vector1, Vector3 vector2)

MethodMeanErrorStdDevRatioRatioSD
Before1.3289 ns0.0531 ns0.0708 ns1.000.00
After0.6613 ns0.0399 ns0.0833 ns0.510.05

Vector3.Transform(Vector3 value, Quaternion rotation)

MethodMeanErrorStdDevRatioRatioSD
Before3.922 ns0.1034 ns0.1380 ns1.000.00
After1.258 ns0.0508 ns0.0776 ns0.320.02

Quaternion operator *(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
dotnet84.0531 ns0.1008 ns0.0990 ns1.000.00
dotnet90.7203 ns0.0312 ns0.0292 ns0.180.01
after0.5752 ns0.0185 ns0.0173 ns0.140.00

Quaternion operator /(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before6.010 ns0.1399 ns0.1240 ns1.000.00
after2.377 ns0.0334 ns0.0296 ns0.400.01

Quaternion Concatenate(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before4.166 ns0.0344 ns0.0321 ns0.690.02
after1.266 ns0.0443 ns0.0414 ns0.210.01

Impl operator *(in Impl left, in Impl right)

MethodMeanErrorStdDevRatio
before8.009 ns0.0856 ns0.0801 ns1.00
afterVector1285.407 ns0.0604 ns0.0565 ns0.68
afterVector2563.142 ns0.0616 ns0.0546 ns0.39

Vector3.Cross(Vector3 vector1, Vector3 vector2)
Vector3.Transform(Vector3 value, Quaternion rotation)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

added simd paths for quaternion multiplication, division and concatenate
added simd path for matrix4x4 multiplication
@martenfmartenf changed the title Added SIMD path for Vector3.Cross and Vector3.TransformAdded SIMD path for Vector3, Quaternion and Matrix4x4 operationsMar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet/area-system-numerics

Comment on lines +531 to +546
if (Vector128.IsHardwareAccelerated)
{
var vVector = value.AsVector128();
var rVector = Unsafe.BitCast<Quaternion, Vector128<float>>(rotation);

// v + 2 * (q x (q.W * v + q x v)
return (vVector + Vector128.Create(2f) * Cross(rVector, Vector128.Shuffle(rVector, Vector128.Create(3, 3, 3, 3)) * vVector + Cross(rVector, vVector))).AsVector3();

static Vector128<float> Cross(Vector128<float> v1, Vector128<float> v2)
{
return (Vector128.Shuffle(v1, Vector128.Create(1, 2, 0, 3)) *
Vector128.Shuffle(v2, Vector128.Create(2, 0, 1, 3))) -
(Vector128.Shuffle(v1, Vector128.Create(2, 0, 1, 3)) *
Vector128.Shuffle(v2, Vector128.Create(1, 2, 0, 3)));
}
}

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.

I'm not immediately following where this implementation is from...

The standard naive algorithm is typically broken down to something like:

return((Quaternion.Conjugate(rotation)*newQuaternion(value,0.0f))*rotation).AsVector128().AsVector3();

(noting that Quaternion.operator *(Quaternion, Quaterion) is not currently marked as aggressively inline)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I got it from a private C library I wrote a decade ago which quotes a dead link.
The closest I could find on the internet after a quick search is this forum post which gets 95% of the way there and just misses the last 'simplification for readability' step.

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.

Can we update this to follow the "standard" algorithm I gave above.

I expect we'll get better support for things like constant folding, more consistent performance across a range of hardware, and we can accurately quote the root implementation to a known source (which avoids any potential licensing issues): https://github.com/microsoft/DirectXMath/blob/main/Inc/DirectXMathVector.inl#L10307-L10317

IIRC, glm uses a similar algorithm for it's implementation

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication. It will also always be slower then the reduced version because it contains significantly more math operation.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

To reduce any variation between different archs the implementation could be changed to

return (vVector + 2 * Cross(rVector, Vector128.Create(rotation.W) * vVector + Cross(rVector, vVector))).AsVector3();

which would give the JIT a bit more leeway, but I doubt it makes any differens.

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.

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication

This is in part because quaternion multiplication is not being inlined currently. But, also because it's doing some "unnecessary" work, as you indicated. But that can be accounted for as well.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

In general we require correct attribution to exist and based on the statement given above, the code (even if you authored it) was itself based on some internet blog/forum/site/etc post that can no longer be referenced (due to a dead link). Such a post may have itself may have been based on something else which overall makes it very difficult for us to use "safely" as the chain of citations is broken.

The actual requirements for attribution, whether or not something can be copyrighted, and the like can get quite complex. When there isn't a clear chain anymore, then it requires additional effort on our part, potentially even requiring checks with legal, so an actual lawyer can make the determination on whether or not it is safe to do.

.NET is a huge open source project depending on by millions. We must do the due diligence and ensure that all the boxes are checked.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

What makes the result of Q*vQ any more correct that any of the other ways you could use to rotate a vector by the rotation saved in a quaternion? Rodrigues' formula is actually older than Hamilton's, but it's not like any of this is a universal law written in the stars. This entire conversation feels pointless so I will stop wasting everyone's time, close this pull request and just use a private library.

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.

From a mathematical perspective they all produce the "same" result. The consideration is that they are not the same from a programmatic perspective due to additional considerations.

The intent was to push this PR in a direction where we could still improve the performance but without changing certain invariants that are extremely important to maintain. That requires more in depth consideration and documentation to show how those requirements are being met.

The intent was not to waste time or get into an argument over what is right vs wrong. The contribution was appreciated and was otherwise nearly in the right shape to be taken.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I know and appreciate that you weren't trying to waste any time, but I just don't have the spare time to have long debates about computational vs mathematical correctness right now, so I think it's just easier if I leave it at that so that someone else can make a pull request if they run into the same bottlenecks.

@tannergoodingtannergoodingJun 14, 2024

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.

👍, are you fine with me cherry-picking your commits into a PR so I can fix the last couple bits of feedback and get merged? That way you can still get credit for the overall contribution

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment on lines +163 to +178
var l0012 = Vector128.Shuffle(left, Vector128.Create(0, 0, 1, 2));
var l1120 = Vector128.Shuffle(left, Vector128.Create(1, 1, 2, 0));
var r0333 = Vector128.Shuffle(right, Vector128.Create(0, 3, 3, 3));
var r1201 = Vector128.Shuffle(right, Vector128.Create(1, 2, 0, 1));

var t0 = l0012 * r0333 + l1120 * r1201;
var mask = Vector128.Create(0x80000000, 0, 0, 0);
var t0m = Vector128.Xor(t0, mask.AsSingle());

var l2201 = Vector128.Shuffle(left, Vector128.Create(2, 2, 0, 1));
var l3333 = Vector128.Shuffle(left, Vector128.Create(3, 3, 3, 3));
var r2120 = Vector128.Shuffle(right, Vector128.Create(2, 1, 2, 0));
var r3012 = Vector128.Shuffle(right, Vector128.Create(3, 0, 1, 2));

var t1 = l3333 * r3012 - l2201 * r2120 + t0m;
var result = Vector128.Shuffle(t1, Vector128.Create(1, 2, 3, 0));

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.

This new code is much harder to read for basically a cycle difference in perf (numbers above report 0.7 -> 0.5 ns). It may also not optimize as well when considering all platforms or ISA baselines (SSE2, SSE4.1, AVX2, AVX512F, AdvSimd, WASM, etc).

It's also substantially different from Concatenate despite them doing the "same thing" (just one is x * y and the other is y * x)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's an operation that tends to be in the hot path for 3d applications so thought the reduced readability might be worth the extra performance on all the platforms I tested it on.

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.

Its not that hot for 3D apps. The actual hot code tends to happen on the GPU instead.

Its especially not so hot that the complexity is worth 0.15ns (which is effectively 1 CPU cycle).

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.

This comment is still pending. I don't think the new code is worth the 0.15ns savings (it's notably slightly slower on my box too). The code is more complex, harder to follow, and so I don't think the justification is there to take this part of the change.

.NET is huge, it is millions of lines of code. We ultimately have to balance readability, maintainability, security, perf, etc.

While Vector3 is itself a performance oriented type, we still have to factor in the other considerations and balance them out. It also applies beyond what a simple benchmark may measure, as we have multiple platforms, ISAs, and ABIs to consider. We also have to consider how certain optimizations may function (such as inlining and constant folding).

As such, even with it being a performance oriented type, there's many places where we are happy to give up a half nanosecond (which is in practice 1-5 instruction cycles) to improve the other areas.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
@tannergooding

Copy link
Copy Markdown
Member

@martenf is this something you're still working on? There's still some pending feedback above that needs to be resolved, mostly just around the tradeoff of additional complexity vs perf increase (that is, where the additional complexity isn't worth the 1-2 instruction or 0.1-0.2 nanosecond savings, so using a more readable/maintainable/portable implementation is preferred).

@tannergoodingtannergooding added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

Sorry I was quite busy the last couple of weeks.
I think that except for Vector3.Transform they are all resolved, just not marked as resolved.

@dotnet-policy-servicedotnet-policy-serviceBot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenfmartenf closed this Jun 13, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 14, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Numericscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@martenf@tannergooding
, '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

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations - #99547

Closed
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd
Closed

Added SIMD path for Vector3, Quaternion and Matrix4x4 operations#99547
martenf wants to merge 7 commits into
dotnet:mainfrom
martenf:VectorSimd

Conversation

@martenf

@martenfmartenf commented Mar 11, 2024

Copy link
Copy Markdown
Contributor

Added SIMD path using Vector128 (and Vector256 for Matrix4x4) for:

Vector3.Cross(Vector3 vector1, Vector3 vector2)

MethodMeanErrorStdDevRatioRatioSD
Before1.3289 ns0.0531 ns0.0708 ns1.000.00
After0.6613 ns0.0399 ns0.0833 ns0.510.05

Vector3.Transform(Vector3 value, Quaternion rotation)

MethodMeanErrorStdDevRatioRatioSD
Before3.922 ns0.1034 ns0.1380 ns1.000.00
After1.258 ns0.0508 ns0.0776 ns0.320.02

Quaternion operator *(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
dotnet84.0531 ns0.1008 ns0.0990 ns1.000.00
dotnet90.7203 ns0.0312 ns0.0292 ns0.180.01
after0.5752 ns0.0185 ns0.0173 ns0.140.00

Quaternion operator /(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before6.010 ns0.1399 ns0.1240 ns1.000.00
after2.377 ns0.0334 ns0.0296 ns0.400.01

Quaternion Concatenate(Quaternion value1, Quaternion value2)

MethodMeanErrorStdDevRatioRatioSD
before4.166 ns0.0344 ns0.0321 ns0.690.02
after1.266 ns0.0443 ns0.0414 ns0.210.01

Impl operator *(in Impl left, in Impl right)

MethodMeanErrorStdDevRatio
before8.009 ns0.0856 ns0.0801 ns1.00
afterVector1285.407 ns0.0604 ns0.0565 ns0.68
afterVector2563.142 ns0.0616 ns0.0546 ns0.39

Vector3.Cross(Vector3 vector1, Vector3 vector2)
Vector3.Transform(Vector3 value, Quaternion rotation)
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

added simd paths for quaternion multiplication, division and concatenate
added simd path for matrix4x4 multiplication
@martenfmartenf changed the title Added SIMD path for Vector3.Cross and Vector3.TransformAdded SIMD path for Vector3, Quaternion and Matrix4x4 operationsMar 11, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

@dotnet/area-system-numerics

Comment on lines +531 to +546
if (Vector128.IsHardwareAccelerated)
{
var vVector = value.AsVector128();
var rVector = Unsafe.BitCast<Quaternion, Vector128<float>>(rotation);

// v + 2 * (q x (q.W * v + q x v)
return (vVector + Vector128.Create(2f) * Cross(rVector, Vector128.Shuffle(rVector, Vector128.Create(3, 3, 3, 3)) * vVector + Cross(rVector, vVector))).AsVector3();

static Vector128<float> Cross(Vector128<float> v1, Vector128<float> v2)
{
return (Vector128.Shuffle(v1, Vector128.Create(1, 2, 0, 3)) *
Vector128.Shuffle(v2, Vector128.Create(2, 0, 1, 3))) -
(Vector128.Shuffle(v1, Vector128.Create(2, 0, 1, 3)) *
Vector128.Shuffle(v2, Vector128.Create(1, 2, 0, 3)));
}
}

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.

I'm not immediately following where this implementation is from...

The standard naive algorithm is typically broken down to something like:

return((Quaternion.Conjugate(rotation)*newQuaternion(value,0.0f))*rotation).AsVector128().AsVector3();

(noting that Quaternion.operator *(Quaternion, Quaterion) is not currently marked as aggressively inline)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I got it from a private C library I wrote a decade ago which quotes a dead link.
The closest I could find on the internet after a quick search is this forum post which gets 95% of the way there and just misses the last 'simplification for readability' step.

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.

Can we update this to follow the "standard" algorithm I gave above.

I expect we'll get better support for things like constant folding, more consistent performance across a range of hardware, and we can accurately quote the root implementation to a known source (which avoids any potential licensing issues): https://github.com/microsoft/DirectXMath/blob/main/Inc/DirectXMathVector.inl#L10307-L10317

IIRC, glm uses a similar algorithm for it's implementation

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication. It will also always be slower then the reduced version because it contains significantly more math operation.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

To reduce any variation between different archs the implementation could be changed to

return (vVector + 2 * Cross(rVector, Vector128.Create(rotation.W) * vVector + Cross(rVector, vVector))).AsVector3();

which would give the JIT a bit more leeway, but I doubt it makes any differens.

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.

The naive implementation is even slower then the dotnet8 implementation, even with the dotnet9 improvements to quaternion multiplication

This is in part because quaternion multiplication is not being inlined currently. But, also because it's doing some "unnecessary" work, as you indicated. But that can be accounted for as well.

I also don't see any potential licensing problem. I wrote the c# code. Btw it looks nothing like c code I based it on which I also wrote. Both version are based on math that cannot be copyrighted and that has been floating around the internet for at least decade at this point.

In general we require correct attribution to exist and based on the statement given above, the code (even if you authored it) was itself based on some internet blog/forum/site/etc post that can no longer be referenced (due to a dead link). Such a post may have itself may have been based on something else which overall makes it very difficult for us to use "safely" as the chain of citations is broken.

The actual requirements for attribution, whether or not something can be copyrighted, and the like can get quite complex. When there isn't a clear chain anymore, then it requires additional effort on our part, potentially even requiring checks with legal, so an actual lawyer can make the determination on whether or not it is safe to do.

.NET is a huge open source project depending on by millions. We must do the due diligence and ensure that all the boxes are checked.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

What makes the result of Q*vQ any more correct that any of the other ways you could use to rotate a vector by the rotation saved in a quaternion? Rodrigues' formula is actually older than Hamilton's, but it's not like any of this is a universal law written in the stars. This entire conversation feels pointless so I will stop wasting everyone's time, close this pull request and just use a private library.

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.

From a mathematical perspective they all produce the "same" result. The consideration is that they are not the same from a programmatic perspective due to additional considerations.

The intent was to push this PR in a direction where we could still improve the performance but without changing certain invariants that are extremely important to maintain. That requires more in depth consideration and documentation to show how those requirements are being met.

The intent was not to waste time or get into an argument over what is right vs wrong. The contribution was appreciated and was otherwise nearly in the right shape to be taken.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I know and appreciate that you weren't trying to waste any time, but I just don't have the spare time to have long debates about computational vs mathematical correctness right now, so I think it's just easier if I leave it at that so that someone else can make a pull request if they run into the same bottlenecks.

@tannergoodingtannergoodingJun 14, 2024

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.

👍, are you fine with me cherry-picking your commits into a PR so I can fix the last couple bits of feedback and get merged? That way you can still get credit for the overall contribution

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Sure

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment on lines +163 to +178
var l0012 = Vector128.Shuffle(left, Vector128.Create(0, 0, 1, 2));
var l1120 = Vector128.Shuffle(left, Vector128.Create(1, 1, 2, 0));
var r0333 = Vector128.Shuffle(right, Vector128.Create(0, 3, 3, 3));
var r1201 = Vector128.Shuffle(right, Vector128.Create(1, 2, 0, 1));

var t0 = l0012 * r0333 + l1120 * r1201;
var mask = Vector128.Create(0x80000000, 0, 0, 0);
var t0m = Vector128.Xor(t0, mask.AsSingle());

var l2201 = Vector128.Shuffle(left, Vector128.Create(2, 2, 0, 1));
var l3333 = Vector128.Shuffle(left, Vector128.Create(3, 3, 3, 3));
var r2120 = Vector128.Shuffle(right, Vector128.Create(2, 1, 2, 0));
var r3012 = Vector128.Shuffle(right, Vector128.Create(3, 0, 1, 2));

var t1 = l3333 * r3012 - l2201 * r2120 + t0m;
var result = Vector128.Shuffle(t1, Vector128.Create(1, 2, 3, 0));

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.

This new code is much harder to read for basically a cycle difference in perf (numbers above report 0.7 -> 0.5 ns). It may also not optimize as well when considering all platforms or ISA baselines (SSE2, SSE4.1, AVX2, AVX512F, AdvSimd, WASM, etc).

It's also substantially different from Concatenate despite them doing the "same thing" (just one is x * y and the other is y * x)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

It's an operation that tends to be in the hot path for 3d applications so thought the reduced readability might be worth the extra performance on all the platforms I tested it on.

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.

Its not that hot for 3D apps. The actual hot code tends to happen on the GPU instead.

Its especially not so hot that the complexity is worth 0.15ns (which is effectively 1 CPU cycle).

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.

This comment is still pending. I don't think the new code is worth the 0.15ns savings (it's notably slightly slower on my box too). The code is more complex, harder to follow, and so I don't think the justification is there to take this part of the change.

.NET is huge, it is millions of lines of code. We ultimately have to balance readability, maintainability, security, perf, etc.

While Vector3 is itself a performance oriented type, we still have to factor in the other considerations and balance them out. It also applies beyond what a simple benchmark may measure, as we have multiple platforms, ISAs, and ABIs to consider. We also have to consider how certain optimizations may function (such as inlining and constant folding).

As such, even with it being a performance oriented type, there's many places where we are happy to give up a half nanosecond (which is in practice 1-5 instruction cycles) to improve the other areas.

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Quaternion.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Numerics/Matrix4x4.Impl.cs Outdated
@tannergooding

Copy link
Copy Markdown
Member

@martenf is this something you're still working on? There's still some pending feedback above that needs to be resolved, mostly just around the tradeoff of additional complexity vs perf increase (that is, where the additional complexity isn't worth the 1-2 instruction or 0.1-0.2 nanosecond savings, so using a more readable/maintainable/portable implementation is preferred).

@tannergoodingtannergooding added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenf

Copy link
Copy Markdown
ContributorAuthor

Sorry I was quite busy the last couple of weeks.
I think that except for Vector3.Transform they are all resolved, just not marked as resolved.

@dotnet-policy-servicedotnet-policy-serviceBot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 4, 2024
@martenfmartenf closed this Jun 13, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 14, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Numericscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@martenf@tannergooding