-
Notifications
You must be signed in to change notification settings - Fork 639
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 delegate based debugging assert #373
Remove delegate based debugging assert #373
Conversation
looks good, this will save allocations. |
This looks like a pretty good way to accomplish the best performance both in production and in tests. |
…ndition before checking for AssertsEnabled
@eladmarg @NightOwl888 think I'm done with the changes, for reviewing, worth checking just the final diff against master as I did some cleanup to make the final diff smaller. |
…ed-debugging-assert
This closes #346 as well... |
I am trying to understand the design choices here, it seems somewhat messy (No offense). You have implemented a "ShouldAssert", which internally checks the flag AssertsEnabled and then the boolean passed, yet in many places we see:
Which means we run the check against
Also note that if if the aim here is to increase performance and reduce allocations, something indicates that string interpolation may be a better candidate than string format for this particular design. Obviously string format would have it's advantages if we were just to rewrite the design by @NightOwl888 from taking a lambda to take a string format and then args. BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1082 (1909/November2018Update/19H2)
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
[Host] : .NET Framework 4.8 (4.8.4220.0), X86 LegacyJIT
DefaultJob : .NET Framework 4.8 (4.8.4220.0), X86 LegacyJIT
|
Hi @jeme, no worries, I also found the final code a bit confusing. The reason for the redundant check is that I didn't want to change the behavior from: Check Flag ----> Check if Assert condition is true ----> Throw To: Check if Assert condition is true ----> Check Flag ----> Throw This would happen if we only have the ShouldAssert call - even with the aggressive inlining option on the ShouldAssert method, the compiler will obviously not invert the checks as that could change the code behavior. if(Debugging.ShouldAssert(*** Some Check ***)){} Thus I added back the Debugging.AssertsEnabled check before the call to ShouldAssert to keep it consistent with the original code, and to avoid the extra checks due to the condition on every call even when Debugging.AssertsEnabled = false. if(Debugging.AssertsEnabled && Debugging.ShouldAssert(*** Some Check ***)){} Regarding the interpolated string, true, with the new design where the check for the condition is outside of the code, that would work fine - I could change it to use the interpolated string again instead of the new string.Format design... |
This feature should be optimized for:
An exception will only occur in an application that both has asserts enabled and is misbehaving in some way. This PR has changed a bit from the Assert<T0>(bool condition, string messageFormat, T0 arg0);
Assert<T0, T1>(bool condition, string messageFormat, T0 arg0, T1 arg1);
Assert<T0, T1, T2>(bool condition, string messageFormat, T0 arg0, T1 arg1, T2 arg2); The above signatures would prevent the string from being constructed until after the condition is checked, which is what we need to avoid unnecessary string concatenation/formatting. We can live with boxing when formatting strings that include value type parameters in this scenario (using For the same reason, which string concatenation method that is used is irrelevant, as string concatenation will only occur in a misbehaving application. Ideally, the string concatenation will also only occur if the condition fails and should never happen when asserts are disabled. I have to agree that there doesn't seem to be much value in // Before
if (Debugging.AssertsEnabled) Debugging.Assert(outputLen > 0, () => "output contains empty string: " + scratchChars);
// After
if (Debugging.AssertsEnabled && !(outputLen > 0)) throw new AssertionException($"output contains empty string: {scratchChars}"); This keeps the syntax relatable to both Java and .NET asserts:
Sure, it is more verbose, but it is what is needed to make the feature as cheap as possible. From #346 (comment):
I suspect not. It would mean that any guard clause would cause a method not to be inlined. Consider: public void Foo(string p1)
{
if (p1 is null)
throw new ArgumentNullException(nameof(p1));
// Implementation
} This is really no different than: public void Foo(string p1)
{
if (Debugging.AssertsEnabled && !p1.Contains("0"))
throw new AssertionException();
// Implementation
} Both of them check a condition, then throw an exception if the condition fails. In addition, the failure is expected to be an extreme edge case so any overhead of concatenating an error message doesn't occur normally. In fact, about 10% of all of the asserts in Lucene could be converted to guard clauses in .NET. For end users, it would be more intuitive to make them guard clauses rather than asserts, but it would come at a runtime performance cost. The point is the asserts are designed to take the place of guard clauses when asserts are enabled, but unlike guard clauses asserts can be turned off in production to improve performance. I am still debating whether to go with asserts or guard clauses, but I am leaning toward making them guard clauses because .NET users don't normally expect to have to "turn on" these checks when debugging.
The original design was to have a few Having a second check is redundant, but the redundancy only occurs when asserts are enabled (during testing/debugging). The redundancy was removed in places where there are blocks of 2 or more Boolean SwitchOne thing that was missed in the current implementation is the fact that .NET already has a built-in feature to turn something on/off from outside of the application for debugging purposes: internal static BooleanSwitch asserts = new BooleanSwitch("assertsEnabled", "Enable Assertions", "false");
public static void MyMethod(string location) {
//Insert code here to handle processing.
if(asserts.Enabled && !<condition>)
throw new AssertionException("Error happened at " + location);
} To utilize this, we need to ensure that it is disabled by default in the core library and enabled by default in the test framework and is correctly passed into the application in the Azure DevOps pipeline tests.
Assert(false)Note that there are many places in Lucene that where the line throw new AssertionError(); was replaced with either: Debugging.Assert(false);
// or
throw new InvalidOperationException(); Essentially, these were both done to compensate for the fact that
|
But we have to choose between Simplicity or Performance in this particular case... The attempt here looks like you went for simplicity first, but then in adding in the performance after that it completely nullified the simplicity attempt, and even made it much much worse. So we have to choose...
If the "Check" is costly, this will hurt performance regardless of Asserts being enabled or not. Or
In the later you will quickly realize that we are essentially passing a boolean to a method just to return it, so the method becomes irrelevant all together and only adds noise, hence we can eliminate that to:
This saves the computation of the check (in normal mode, where asserts are disabled), if it's costly that matters, otherwise it is negligible. Obviously we can then as a final step simply inline the throw of the exception. I have no opinion on that here. The example overloads like @NightOwl888 presented:
Should avoid the extra allocations as well and is the option most in line with what is currently in place. This is also what is most in line with the built-in "Debug.Assert". I would as much as anyone like to avoid all this mess all-together, but the only solution that would be somewhat "Clean" to the code I can think of is some heavy reliance of AOP and that is complicated to add. |
I neglected to mention that in the original implementation, the Originally, all of the Both of the above changes brought the performance pretty close to what it was when we used But this is just more proof that not all of these asserts are equal. For example:
Unfortunately, going down this road is inevitably going to lead to many specializations of asserts, some of which diverge from the Lucene source, all in the name of performance.
Well, I was hoping to avoid it, too. Several years ago we assumed that AOP could potentially save us from having to go down the road of specializing asserts in the name of performance. It might be costly, but when compared against trying to maintain a diverging set of asserts and |
There is some fairly mature AOP frameworks out there, however not all will be suitable for what we need here. E.g. I still think PostSharp, which is one of the bigger ones, is focused on Compile time AOP which makes it unsuitable here as we are looking for a Runtime. Harmony works at runtime so that could be a candidate, it's has apparently been mostly used to mod games, but that shouldn't matter. The bigger problem though is that the I was thinking that one approach for this could be to pull out the actual assert into it's own method in each class (A .NET specific method)... That would introduce a call to a "NOOP" by default at runtime, and that could then be replace. The consequence here though is that then we may not even need AOP at all to replace the logic as we can perhaps inject it instead... (But I want to perform some experiments to look into the performance cost of a "NOOP" in various scenarios vs the boolean check we have now.. |
another option is to write custom Fody weaver but I'm not sure we should invest the efforts on this area to gain performance. for instance, there are many places we can save allocations, better re-use of allocated objects using pools and managers. I would even say that in the cases the code isn't in use for testing/validation and only output, we can simply wrap it with conditional #IF TEST. (to remove it completely) I'm not sure its that necessary in all the places its used. |
Agreed. But to resolve this PR, I would say the least we should do is eliminate the There are 2 acceptable ways to eliminate the
We can then evaluate whether additional measures to improve performance of this feature are worth the effort. My guess is there will not be any additional measures required. This feature exists in Lucene and they were willing to accept its performance cost in the design as a tradeoff to properly run all of the test conditions when they are needed, including when end users run tests on their own components using the test framework or run |
I would also go for option 1. |
…ters so the parameters are not resolved until a condition fails. There are still some calls that do light math and pick items from arrays, but this performance hit in the tests is something we can live with for better production performance. Closes apache#375, closes apache#373, closes apache#372.
…ters so the parameters are not resolved until a condition fails. There are still some calls that do light math and pick items from arrays, but this performance hit in the tests is something we can live with for better production performance. Closes apache#346, closes apache#373, closes apache#372.
No description provided.