Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove union overlay design and use reflection in SqlTypeWorkarounds #1647

Merged
merged 8 commits into from
Jul 19, 2022

Conversation

David-Engel
Copy link
Contributor

@David-Engel David-Engel commented Jun 17, 2022

This replaces the union overlay design against netfx with reflection queries to see if appropriate internal methods exist and utilizes them. If they don't exist, the code falls back to slower public APIs.

The union overlay design is retained against netcore because most of the internal methods used were not referenced by netcore itself and removed by the netcore build process so they don't actually exist. We will push for public APIs in .NET and migrate to them when they are available. Calling the current public APIs in netcore would result in significant performance penalties in this hot path.

@codecov
Copy link

codecov bot commented Jun 17, 2022

Codecov Report

Merging #1647 (5d71d6e) into main (2512657) will decrease coverage by 0.09%.
The diff coverage is 70.86%.

❗ Current head 5d71d6e differs from pull request most recent head 0c64ab7. Consider uploading reports for the commit 0c64ab7 to get more accurate results

@@            Coverage Diff             @@
##             main    #1647      +/-   ##
==========================================
- Coverage   71.45%   71.35%   -0.10%     
==========================================
  Files         291      293       +2     
  Lines       61241    61368     +127     
==========================================
+ Hits        43757    43792      +35     
- Misses      17484    17576      +92     
Flag Coverage Δ
addons 92.38% <ø> (ø)
netcore 74.89% <84.00%> (-0.13%) ⬇️
netfx 69.21% <68.99%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...icrosoft/Data/SqlTypes/SqlTypeWorkarounds.netfx.cs 68.25% <68.25%> (ø)
...rosoft/Data/SqlTypes/SqlTypeWorkarounds.netcore.cs 81.81% <81.81%> (ø)
.../src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs 71.42% <100.00%> (-4.77%) ⬇️
...src/Microsoft/Data/SqlClient/SqlMetadataFactory.cs 81.66% <0.00%> (-10.84%) ⬇️
...ActiveDirectoryAuthenticationTimeoutRetryHelper.cs 56.81% <0.00%> (-6.82%) ⬇️
...re/src/Microsoft/Data/SqlClient/SNI/SNINpHandle.cs 68.85% <0.00%> (-4.92%) ⬇️
...icrosoft/Data/ProviderBase/DbConnectionInternal.cs 78.22% <0.00%> (-2.42%) ⬇️
...Microsoft/Data/SqlClient/TdsParserStaticMethods.cs 72.22% <0.00%> (-1.12%) ⬇️
.../Microsoft/Data/SqlClient/SNI/SNIMarsConnection.cs 76.72% <0.00%> (-0.63%) ⬇️
...crosoft/Data/SqlClient/SqlInternalConnectionTds.cs 72.88% <0.00%> (-0.40%) ⬇️
... and 10 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2512657...0c64ab7. Read the comment docs.

@Wraith2
Copy link
Contributor

Wraith2 commented Jun 17, 2022

This will be slower and still relies on internal implementation details of the runtime types. The overlay trick itself isn't a problem it's the fact that it requires private access that is, isn't it?

@GrabYourPitchforks
Copy link
Member

The overlay trick itself isn't a problem it's the fact that it requires private access that is, isn't it?

The overlay trick is too fragile. Private reflection is ok as long as the target method is documented (see dotnet/docs#28911, dotnet/docs#29900). In the case of the ref emit trick, it takes advantage of the fact that the type's fields are implicitly documented as part of its serialization contract (see GetFastDecomposers).

@GrabYourPitchforks
Copy link
Member

@David-Engel Calling MethodInfo.Invoke in a hot path is going to be slow. However, once you have the MethodInfo in hand, you can turn it into a delegate and cache the delegate, allowing you to call it over and over again. Delegate invocation should be very fast.

Sample code:

private delegate long SqlMoneyToLongDelegate(ref SqlMoney @this);

var mi = GetFastSqlMoneyToLong();
if (mi is null) { /* fallback */ }

// On Full Framework, invoking the MethodInfo first before wrapping
// a delegate around it will produce better codegen. We don't need
// to inspect the return value; we just need to call the method.

_ = mi.Invoke(new SqlMoney(0), new object[0]);

// Now create the delegate. This is an open delegate, meaning the
// "this" parameter will be provided as arg0 on each call.

var del = (SqlMoneyToLongDelegate)mi.CreateDelegate(typeof(SqlMoneyToLongDelegate), target: null);

// Now we can cache the delegate and invoke it over and over again.
// Note: the first parameter to the delegate is provided *byref*.

SqlMoney money = new SqlMoney(10);
long l1 = del(ref money); // returns 100_000

money = new SqlMoney(20);
long l2 = del(ref money); // returns 200_000

@David-Engel
Copy link
Contributor Author

@David-Engel Calling MethodInfo.Invoke in a hot path is going to be slow. However, once you have the MethodInfo in hand, you can turn it into a delegate and cache the delegate, allowing you to call it over and over again. Delegate invocation should be very fast.

Sample code:

private delegate long SqlMoneyToLongDelegate(ref SqlMoney @this);

var mi = GetFastSqlMoneyToLong();
if (mi is null) { /* fallback */ }

// On Full Framework, invoking the MethodInfo first before wrapping
// a delegate around it will produce better codegen. We don't need
// to inspect the return value; we just need to call the method.

_ = mi.Invoke(new SqlMoney(0), new object[0]);

// Now create the delegate. This is an open delegate, meaning the
// "this" parameter will be provided as arg0 on each call.

var del = (SqlMoneyToLongDelegate)mi.CreateDelegate(typeof(SqlMoneyToLongDelegate), target: null);

// Now we can cache the delegate and invoke it over and over again.
// Note: the first parameter to the delegate is provided *byref*.

SqlMoney money = new SqlMoney(10);
long l1 = del(ref money); // returns 100_000

money = new SqlMoney(20);
long l2 = del(ref money); // returns 200_000

@GrabYourPitchforks
Thanks. I couldn't use the IlGenerator sample because DynamicMethod isn't supported in .NET Standard 2.0. I'll see if I can get this design to work.

@GrabYourPitchforks
Copy link
Member

I couldn't use the IlGenerator sample because DynamicMethod isn't supported in .NET Standard 2.0. I'll see if I can get this design to work.

In ns2.0 you need to pull in the packages https://www.nuget.org/packages/System.Reflection.Emit.ILGeneration and https://www.nuget.org/packages/System.Reflection.Emit.Lightweight to get ILGenerator to work.

Remember: the only requirement is to avoid using the union overlay trick when running within a Full Framework application. (If you're deployed as a netstandard2.0 package and running within Full Framework, you also have to avoid using union overlays.) If you have some other reliable mechanism to detect that you're not running within a Full Framework application, feel free to use whatever trick you want if it helps simplify your code. A union overlay trick when running within .NET Core would still be fragile and not recommended, but it's not prohibited.

@GrabYourPitchforks
Copy link
Member

We should also consider reopening dotnet/runtime#51836 to avoid the need for these types of tricks in the future. Some of the APIs might not be needed - for example, SqlGuid could change to be backed by a single Nullable<Guid> field under the covers. But I think this exercise has shown the need for the bulk of them.

@David-Engel
Copy link
Contributor Author

David-Engel commented Jun 21, 2022

I ran some perf tests on the SqlTypesWorkaround methods to see what the difference would be and the results were interesting. Not nearly as consistent as I expected.

Original represents using the union overlay design. New represents reflection to call internal methods. Fallback represents all public APIs. Note: "New" on .NET Core and .NET actually results in Fallback public APIs after reflection as the internal methods are not actually present in the final binaries since they get stripped due to not being referenced.

Numbers are milliseconds to complete 100,000,000 iterations over the methods. Lower is better.

========================================== .NET Framework

Method Original New Fallback
SqlBinaryCtor 1479 12226 51626
SqlDecimalExtractData 2290 3183 4548
SqlGuidCtor 100 634 3519
SqlMoneyCtor 2692 6213 70361
SqlMoneyToInternal 1731 1641 1800

========================================== .NET Core 3.1

Method Original New Fallback
SqlBinaryCtor 792 35102 29671
SqlDecimalExtractData 198 6172 7673
SqlGuidCtor 138 6531 5378
SqlMoneyCtor 1687 7666 49459
SqlMoneyToInternal 1339 23320 20444

========================================== .NET 6

Method Original New Fallback
SqlBinaryCtor 432 18354 20521
SqlDecimalExtractData 512 6684 3130
SqlGuidCtor 27 1036 996
SqlMoneyCtor 503 4239 24299
SqlMoneyToInternal 1027 14956 16348

Perf code for reference is here:
SqlTypesWorkaroundPerf.zip

@Wraith2
Copy link
Contributor

Wraith2 commented Jun 21, 2022

I'm not sure how to interpret the numbers, are they generally acceptible?

@David-Engel
Copy link
Contributor Author

I'm not sure how to interpret the numbers, are they generally acceptible?

Numbers are in millis. Lower is better. Simple loop of 100,000,000 iterations over each method. Perf isn't my expertise. Feel free to take my zip file and tell me I'm doing it wrong. 😉

@Wraith2
Copy link
Contributor

Wraith2 commented Jun 21, 2022

So SqlBinaryCtor is 42 times slower on net6? that doesn't seem like a performance hit that users will live with.
In terms of benchmarking try using BDN because it'll handle warmup and iteration counts for you. Also try to use benchmark setup and avoid manual allocations in the benchmark methods themselves.

@David-Engel
Copy link
Contributor Author

So SqlBinaryCtor is 42 times slower on net6? that doesn't seem like a performance hit that users will live with. In terms of benchmarking try using BDN because it'll handle warmup and iteration counts for you. Also try to use benchmark setup and avoid manual allocations in the benchmark methods themselves.

I agree. I've been debugging and I found one bug (pushed that change) but the main reason it's so slow on netcore31 and net6 is that CtorHelper.CreateFactory is not finding the constructors against those targets the same way it's finding them for netfx.

                    ConstructorInfo fullCtor = typeof(TInstance).GetConstructor(
                        BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.ExactBinding,
                        null, new[] { typeof(TValue), typeof(TIgnored) }, null);

The first time through CtorHelper.CreateFactory, it seems to work for the first method it's looking for, but subsequent passes through there result in a null fullCtor.

It works for netfx. I'm getting frustrated trying to figure out why. The internal methods are still there on netcoreapp31 and net6 (I realize the net6 one now has a nullable byte[]? parameter but that doesn't explain netcoreapp31). Still working on it. I feel like I need to move things around to see if I'm trying to avoid a .NET bug somewhere.

@GrabYourPitchforks Any ideas?

@Wraith2
Copy link
Contributor

Wraith2 commented Jun 22, 2022

SqlBinary doesn't have a 2 parameter ctor, it only has two byte[] and boolean.

@GrabYourPitchforks
Copy link
Member

@David-Engel Looks like those methods were scrubbed from the .NET Core implementation before it shipped. There's a post-build tool run over .NET Core which looks for uncalled methods and prunes them from the product. Since that ctor was internal and had no visible callers, and since the assembly has no [InternalsVisibleTo] attribute, the tool believed the ctor to be dead and automatically pruned it.

If desired, you can keep the existing overlay trick for netcore. The only place the overlay trick is forbidden is in the netfx implementation, but I think your testing shows that things are within acceptable limits there.

For netcore, the overlay would still be very fragile (what happens if SqlGuid is changed to contain the data inline? your app will probably start AVing). So it's in everybody's best interests to approve proper, supported APIs for your scenario ASAP.

@David-Engel
Copy link
Contributor Author

@JRahnama @DavoudEshtehari @Kaur-Parminder This PR is ready for review now.

@David-Engel
Copy link
Contributor Author

David-Engel commented Jul 1, 2022

Ran perf numbers with BDN. *New methods try to use reflection and internal methods. *Orig methods use the union overlay hacks. *Slow methods use only public APIs.

Since we are now keeping the original design for .NET 3.1 and .NET 6, the relevant numbers are the ones for .NET Framework 4.8. I just kept the others in there for reference.

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1766 (21H1/May2021Update)
Intel Core i5-4590 CPU 3.30GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
[Host] : .NET Framework 4.8 (4.8.4515.0), X64 RyuJIT
.NET 6.0 : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
.NET Core 3.1 : .NET Core 3.1.26 (CoreCLR 4.700.22.26002, CoreFX 4.700.22.26801), X64 RyuJIT
.NET Framework 4.8 : .NET Framework 4.8 (4.8.4515.0), X64 RyuJIT

Method Job Runtime Mean Error StdDev
SqlBinaryCtorTestNew .NET 6.0 .NET 6.0 14.1365 ns 0.3034 ns 0.3494 ns
SqlBinaryCtorTestOrig .NET 6.0 .NET 6.0 0.0364 ns 0.0221 ns 0.0206 ns
SqlBinaryCtorTestSlow .NET 6.0 .NET 6.0 13.3549 ns 0.2969 ns 0.3300 ns
SqlDecimaExtractDataTestNew .NET 6.0 .NET 6.0 19.9312 ns 0.4531 ns 0.4450 ns
SqlDecimalExtractDataTestOrig .NET 6.0 .NET 6.0 7.2099 ns 0.0755 ns 0.0706 ns
SqlDecimalExtractDataTestSlow .NET 6.0 .NET 6.0 16.0824 ns 0.0776 ns 0.0726 ns
SqlGuidCtorTestNew .NET 6.0 .NET 6.0 14.0562 ns 0.2942 ns 0.4915 ns
SqlGuidCtorTestOrig .NET 6.0 .NET 6.0 0.2720 ns 0.0105 ns 0.0093 ns
SqlGuidCtorTestSlow .NET 6.0 .NET 6.0 12.3584 ns 0.0895 ns 0.0837 ns
SqlMoneyCtorTestNew .NET 6.0 .NET 6.0 8.5601 ns 0.0305 ns 0.0238 ns
SqlMoneyCtorTestOrig .NET 6.0 .NET 6.0 5.2240 ns 0.0057 ns 0.0054 ns
SqlMoneyCtorTestSlow .NET 6.0 .NET 6.0 56.7920 ns 0.1061 ns 0.0992 ns
SqlMoneyToInternalTestNew .NET 6.0 .NET 6.0 50.6023 ns 0.0881 ns 0.0781 ns
SqlMoneyToInternalTestOrig .NET 6.0 .NET 6.0 1.6560 ns 0.0052 ns 0.0046 ns
SqlMoneyToInternalTestSlow .NET 6.0 .NET 6.0 50.6495 ns 0.1239 ns 0.1159 ns
SqlBinaryCtorTestNew .NET Core 3.1 .NET Core 3.1 23.7261 ns 0.1646 ns 0.1374 ns
SqlBinaryCtorTestOrig .NET Core 3.1 .NET Core 3.1 0.1279 ns 0.0013 ns 0.0010 ns
SqlBinaryCtorTestSlow .NET Core 3.1 .NET Core 3.1 22.9754 ns 0.0607 ns 0.0568 ns
SqlDecimaExtractDataTestNew .NET Core 3.1 .NET Core 3.1 19.8899 ns 0.4404 ns 0.4119 ns
SqlDecimalExtractDataTestOrig .NET Core 3.1 .NET Core 3.1 7.4398 ns 0.0390 ns 0.0365 ns
SqlDecimalExtractDataTestSlow .NET Core 3.1 .NET Core 3.1 17.3370 ns 0.0931 ns 0.0871 ns
SqlGuidCtorTestNew .NET Core 3.1 .NET Core 3.1 25.1975 ns 0.0784 ns 0.0695 ns
SqlGuidCtorTestOrig .NET Core 3.1 .NET Core 3.1 0.1460 ns 0.0022 ns 0.0021 ns
SqlGuidCtorTestSlow .NET Core 3.1 .NET Core 3.1 24.0917 ns 0.0753 ns 0.0629 ns
SqlMoneyCtorTestNew .NET Core 3.1 .NET Core 3.1 5.2512 ns 0.0080 ns 0.0071 ns
SqlMoneyCtorTestOrig .NET Core 3.1 .NET Core 3.1 4.6686 ns 0.0125 ns 0.0117 ns
SqlMoneyCtorTestSlow .NET Core 3.1 .NET Core 3.1 78.8679 ns 0.1800 ns 0.1683 ns
SqlMoneyToInternalTestNew .NET Core 3.1 .NET Core 3.1 65.3969 ns 0.1577 ns 0.1398 ns
SqlMoneyToInternalTestOrig .NET Core 3.1 .NET Core 3.1 1.8967 ns 0.0060 ns 0.0053 ns
SqlMoneyToInternalTestSlow .NET Core 3.1 .NET Core 3.1 65.5601 ns 0.1517 ns 0.1419 ns
SqlBinaryCtorTestNew .NET Framework 4.8 .NET Framework 4.8 7.0229 ns 0.0261 ns 0.0244 ns
SqlBinaryCtorTestOrig .NET Framework 4.8 .NET Framework 4.8 0.2985 ns 0.0047 ns 0.0044 ns
SqlBinaryCtorTestSlow .NET Framework 4.8 .NET Framework 4.8 24.2604 ns 0.0593 ns 0.0554 ns
SqlDecimaExtractDataTestNew .NET Framework 4.8 .NET Framework 4.8 17.4681 ns 0.1113 ns 0.1041 ns
SqlDecimalExtractDataTestOrig .NET Framework 4.8 .NET Framework 4.8 13.2396 ns 0.0381 ns 0.0356 ns
SqlDecimalExtractDataTestSlow .NET Framework 4.8 .NET Framework 4.8 23.6965 ns 0.0403 ns 0.0337 ns
SqlGuidCtorTestNew .NET Framework 4.8 .NET Framework 4.8 6.9194 ns 0.0451 ns 0.0422 ns
SqlGuidCtorTestOrig .NET Framework 4.8 .NET Framework 4.8 0.2734 ns 0.0027 ns 0.0024 ns
SqlGuidCtorTestSlow .NET Framework 4.8 .NET Framework 4.8 26.3985 ns 0.0301 ns 0.0235 ns
SqlMoneyCtorTestNew .NET Framework 4.8 .NET Framework 4.8 5.3734 ns 0.0334 ns 0.0312 ns
SqlMoneyCtorTestOrig .NET Framework 4.8 .NET Framework 4.8 4.6980 ns 0.0105 ns 0.0098 ns
SqlMoneyCtorTestSlow .NET Framework 4.8 .NET Framework 4.8 229.3771 ns 0.4492 ns 0.4202 ns
SqlMoneyToInternalTestNew .NET Framework 4.8 .NET Framework 4.8 2.9813 ns 0.0142 ns 0.0133 ns
SqlMoneyToInternalTestOrig .NET Framework 4.8 .NET Framework 4.8 1.9883 ns 0.0031 ns 0.0024 ns
SqlMoneyToInternalTestSlow .NET Framework 4.8 .NET Framework 4.8 2.9624 ns 0.0183 ns 0.0171 ns

Project for reference:
SqlTypeWorkaroundsBenchmarks.zip

Copy link
Contributor

@DavoudEshtehari DavoudEshtehari left a comment

Choose a reason for hiding this comment

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

LGTM, only I'm wondering if there is a way to increase the code coverage on slow paths.

@David-Engel David-Engel merged commit e9101ca into dotnet:main Jul 19, 2022
@David-Engel David-Engel deleted the sqltypes branch July 19, 2022 16:10
David-Engel added a commit to David-Engel/SqlClient that referenced this pull request Aug 9, 2022
David-Engel added a commit to David-Engel/SqlClient that referenced this pull request Aug 9, 2022
David-Engel added a commit that referenced this pull request Aug 10, 2022
DavoudEshtehari pushed a commit to DavoudEshtehari/SqlClient that referenced this pull request Aug 11, 2022
…otnet#1647)

* Remove union overlay design and use reflection in SqlTypeWorkarounds

* Leave union overlay method intact for netcore
# Conflicts:
#	src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
#	src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
DavoudEshtehari pushed a commit to DavoudEshtehari/SqlClient that referenced this pull request Aug 15, 2022
…otnet#1647)

* Remove union overlay design and use reflection in SqlTypeWorkarounds

* Leave union overlay method intact for netcore
# Conflicts:
#	src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
#	src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
DavoudEshtehari pushed a commit to DavoudEshtehari/SqlClient that referenced this pull request Aug 16, 2022
…otnet#1647)

* Remove union overlay design and use reflection in SqlTypeWorkarounds

* Leave union overlay method intact for netcore
# Conflicts:
#	src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
#	src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
#	src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlTypeWorkarounds.cs
DavoudEshtehari added a commit that referenced this pull request Aug 23, 2022
DavoudEshtehari added a commit that referenced this pull request Aug 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants