Don't think of exceptions as business logic; you may not be able to handle them

One: Background

1. Storytelling

After years of crawling around the project, you should have seen more or less people handle exceptions as business logic (), such as determining whether a number is an integer.Just wrap it up in try catch and do it againInt.Parse, if you throw an exception it means it's not an integer, it's rude, it doesn't need to write regular or other logic. Another example is to force a string to be converted to Enum and use Enum.Parse directly. Maybe it's because you don't know the cost of the exception very well. This bad usage habit may have been discovered officially, and we've been supplemented with many methods of Try prefix, such as int.T.RyParse, Enum.TryParse, dict.TryGetValue, shown in code as follows:

            //Original Writing
            var num = int.Parse("1");

            //Use try mode
            var result = 0;
            var b = int.TryParse("1", out result);

There's nothing wrong with the Try series, but it's a groovy way to write it and define the result variable independently. It's up to us, the developers, to promote it.๐Ÿ˜„๐Ÿ˜„๐Ÿ˜„Finally, an out variables grammar sugar was added to C# 7.0.

            //try out variable mode
            var c = int.TryParse("1", out int result2);

This out variable pattern is_Well, one method gets two values without throwing the risk of an exception.

2: Why use tryxxx method

With the tryxxx approach, you should understand that Microsoft is already reminding developers not to abuse exceptions, especially in predictable and predictable scenarios, because they know the cost of exceptions is too high.

1. Low visible performance

To make it visible to the naked eye, we'll make a performance comparison between the exception method and the tryxxx method, iterating 50 weeks to see how well each performs.

            for (int i = 0; i < 3; i++)
            {
                var watch = Stopwatch.StartNew();
                for (int k = 0; k < 50000; k++)
                {
                    try
                    {
                        var num = int.Parse("xxx");
                    }
                    catch (Exception ex) { }
                }
                watch.Stop();

                Console.WriteLine($"i={i + 1},Consumption:{watch.ElapsedMilliseconds}");
            }
            Console.WriteLine("---------------------------------------------");
            for (int i = 0; i < 3; i++)
            {
                var watch = Stopwatch.StartNew();

                for (int k = 0; k < 50000; k++)
                {
                   var num = int.TryParse("xxx", out int reuslt);
                }

                watch.Stop();

                Console.WriteLine($"i={i + 1},Consumption:{watch.ElapsedMilliseconds}");
            }
            Console.ReadLine();

Looking at the result, it's scary, 480 times different, a familiar number.Four hundred and eighty temples in the Southern Dynasty๐Ÿ˜„๐Ÿ˜„๐Ÿ˜„

3. Exceptional Overhead

Why are exceptions so expensive?Only know who you are. Friends who have seen my multi-threaded videos should know that the cost of creating and destroying threads is very large. One of them is to switch code from user state to kernel state. Threads are things at the operating system level, not related to your CLR. CLR just makes a layer of system packaging. In fact, many people can't think of it.The bottom level of try catch finally used also encapsulates the operating system level (Windows Structured Exception Handling), also known as SEH. What does it mean?The cost of switching code from user state to kernel state after you throw is not small. There is also an overhead from StackTrace in Exception, where the value needs to grab the call stack from the thread stack of the current exception. The deeper the stack, the higher the overhead.

1. From User State to Kernel State

You will surely say, let's not be so mysterious. Everything has to be proven. Do more,Talk less, here I'm going to explain it in two ways.

<1>catch case

Prepare to block the catch and grab its dump file.

        public static void Main(string[] args)
        {
            try
            {
                var num = int.Parse("xxx");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadLine();
            }
        }

Use! dumpstack to simplify all managed and unmanaged stacks for thread 0 as follows:

0:000> ~0s
ntdll!NtReadFile+0x14:
00007fff`f805aa64 c3              ret
0:000> !dumpstack
OS Thread Id: 0x2bf0 (0)
Current frame: ntdll!NtReadFile+0x14
Caller, Callee
(MethodDesc 00007fffde3a40b8 +0x18 System.Console.ReadLine())
(MethodDesc 00007fff810d59f8 +0xa5 ConsoleApp4.Program.Main(System.String[])), calling (MethodDesc 00007fffde3a40b8 +0 System.Console.ReadLine())
00000044433fc700 00007fffe07a29e0 clr!ExceptionTracker::CallCatchHandler+0x9c, calling clr!ExceptionTracker::CallHandler
clr!ClrUnwindEx+0x40, calling ntdll!RtlUnwindEx
ntdll!RtlRaiseException+0x4e, calling ntdll!RtlpCaptureContext
clr!IL_Throw+0x114, calling clr!RaiseTheExceptionInternalOnly
(MethodDesc 00007fffde4f95c0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean)), calling mscorlib_ni+0x53976a
(MethodDesc 00007fffde3b5330 +0xae System.Number.ParseInt32(System.String, System.Globalization.NumberStyles, System.Globalization.NumberFormatInfo)), calling (MethodDesc 00007fffde4f95c0 +0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean))
(MethodDesc 00007fffde1ebfa8 +0x2eb System.Globalization.NumberFormatInfo..ctor(System.Globalization.CultureData)), calling (MethodDesc 00007fffde1eba68 +0 System.Globalization.CultureData.GetNFIValues(System.Globalization.NumberFormatInfo))
(MethodDesc 00007fff810d59f8 +0x49 ConsoleApp4.Program.Main(System.String[])), calling (MethodDesc 00007fffde3b1708 +0 System.Int32.Parse(System.String))

Because it's a stack, the execution stream moves backwards and forwards, and you'll find that the process looks like int.Parse -> CLR -> ntdll -> CLR -> Console.ReadLine. It's clear that ntdll.dll is a core file at the operating system level, which cuts from the user state to the kernel state. If you don't know it very well, I'll draw a sketch.

<2>.No catch processing

You must be curious, but you can dig in windbg without catch.

        public static void Main(string[] args)
        {
            var num = int.Parse("xxx");
        }


0:000> !dumpstack
OS Thread Id: 0xd68 (0)
Current frame: ntdll!NtTerminateProcess+0x14
Caller, Callee
mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes+0x285, calling KERNEL32!ExitProcessImplementation
mscoreei!CLRRuntimeHostInternalImpl::ShutdownAllRuntimesThenExit+0x14, calling mscoreei!RuntimeDesc::ShutdownAllActiveRuntimes
clr!EEPolicy::ExitProcessViaShim+0x9c
clr!SafeExitProcess+0x9d, calling clr!EEPolicy::ExitProcessViaShim
ntdll!KiUserExceptionDispatch+0x53, calling ntdll!NtRaiseException
clr!RaiseTheExceptionInternalOnly+0x188426, calling clr!EEPolicy::HandleFatalError
clr!IL_Throw+0x45, calling clr!LazyMachStateCaptureState
(MethodDesc 00007fffde4f95c0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean)), calling mscorlib_ni+0x53976a
(MethodDesc 00007fffde3b5330 +0xae System.Number.ParseInt32(System.String, System.Globalization.NumberStyles, System.Globalization.NumberFormatInfo)), calling (MethodDesc 00007fffde4f95c0 +0 System.Number.StringToNumber(System.String, System.Globalization.NumberStyles, NumberBuffer ByRef, System.Globalization.NumberFormatInfo, Boolean))
(MethodDesc 00007fffde1ebfa8 +0x2eb System.Globalization.NumberFormatInfo..ctor(System.Globalization.CultureData)), calling (MethodDesc 00007fffde1eba68 +0 System.Globalization.CultureData.GetNFIValues(System.Globalization.NumberFormatInfo))
(MethodDesc 00007fff810e59f8 +0x37 ConsoleApp4.Program.Main(System.String[])), calling (MethodDesc 00007fffde3b1708 +0 System.Int32.Parse(System.String))

You can see that the exit logic of the process is given to the managed program entry mscoreei.dll and no longer enters the Main function. So I'll make a picture for you to see.

2. Grab Thread Call Stack

When people panic to see the abnormality, what is the abnormality information at first sight?The second look at what line of code the exception is, which is the thread's call stack. This information is very important to help us find and fix the problem quickly. Place it in StackTrace in Exception and start with the previous section of code.

    public static void Main(string[] args)
        {
            Run();
            Console.ReadLine();
        }

        public static void Run()
        {
            var ex = new FormatException("Your format is wrong!!!");
            throw ex;
        }

<1> When StackTrace was inserted

I haven't seen any book so far about when StackTrace was inserted?Due to the limited level, I also try to detect.

You can see from the code that it was not inserted at the time of new, where would it be?

<2>Find answers from CLR

Since you're not in user code, go to the CLR and use dumpstack in windbg to see the unmanaged stack.

0:000> !dumpstack
OS Thread Id: 0x4090 (0)
Current frame: ntdll!NtTerminateProcess+0x14
Caller, Callee
clr!EETypeHashTable::FindItem+0x532, calling clr!NgenHashTable<EEClassHashTable,EEClassHashEntry,4>::PersistedBucketList::GetBucket
clr!JIT_StrCns+0xd0, calling clr!HelperMethodFrameRestoreState
(MethodDesc 00007fff810f5a08 +0x70 ConsoleApp4.Program.Run()), calling clr!IL_Throw
clr!IL_Throw+0x45, calling clr!LazyMachStateCaptureState
(MethodDesc 00007fff810f5a08 +0x70 ConsoleApp4.Program.Run()), calling clr!IL_Throw
(MethodDesc 00007fff810f59f8 +0x28 ConsoleApp4.Program.Main(System.String[])), calling 00007fff81200488 (stub for ConsoleApp4.Program.Run())

Looking at the simplified process, the suspicion was handled by clr!HelperMethodFrameRestoreState. Why do you say that?Because the FormatException ex we defined will be passed to the CLR, we don't believe we can look at it in kb.

0:000> kb
 # RetAddr           : Args to Child                                                           : Call Site
00 00007fff`e07a3181 : 00000000`e0434352 0000006d`4a7fe938 0000017b`30ad2d48 0000017b`2f081690 : KERNELBASE!RaiseException+0x68
01 00007fff`e07a45f4 : ffffffff`fffffffe 0000017b`2ef02542 00000000`0000000a 0000017b`2f040910 : clr!RaiseTheExceptionInternalOnly+0x31f
02 00007fff`811d0950 : 00000000`70000001 00007fff`810c4140 0000006d`4a7fedb8 0000006d`4a7fec78 : clr!IL_Throw+0x114
03 00007fff`811d08b8 : 0000017b`30ad2d30 00007fff`810c4140 00000000`00000000 00007fff`00000000 : 0x00007fff`811d0950
04 00007fff`e0736c93 : 0000017b`30ad2d30 00007fff`810c4140 00000000`00000000 00007fff`00000000 : 0x00007fff`811d08b8
05 00007fff`e0736b79 : 00000000`00000000 00007fff`e0737aae 0000006d`4a7fefb8 00000000`00000000 : clr!CallDescrWorkerInternal+0x83
06 00007fff`e0737410 : 0000006d`4a7fefb8 0000006d`4a7ff048 0000006d`4a7feeb8 00000000`00000001 : clr!CallDescrWorkerWithHandler+0x4e
07 00007fff`e08dcaf2 : 0000006d`4a7fee00 00000000`00000001 00000000`00000001 0000017b`2efcecf0 : clr!MethodDescCallSite::CallTargetWorker+0x102
08 00007fff`e08dd4b3 : 00000000`00000001 00000000`00000000 0000017b`30ad2d30 0000017b`30ad2d30 : clr!RunMain+0x25f
09 00007fff`e08dd367 : 0000017b`2f040910 0000006d`4a7ff420 0000017b`2f040910 0000017b`2f082770 : clr!Assembly::ExecuteMainMethod+0xb7
0a 00007fff`e08dccb3 : 00000000`00000000 0000017b`2ef00000 00000000`00000000 00000000`00000000 : clr!SystemDomain::ExecuteMainMethod+0x643
0b 00007fff`e08dcc31 : 0000017b`2ef00000 00007fff`e08de090 00000000`00000000 00000000`00000000 : clr!ExecuteEXE+0x3f
0c 00007fff`e08de0a4 : ffffffff`ffffffff 00007fff`e08de090 00000000`00000000 00000000`00000000 : clr!_CorExeMainInternal+0xb2
0d 00007fff`e1208a61 : 00000000`00000000 00007fff`00000091 00000000`00000000 0000006d`4a7ff9f8 : clr!CorExeMain+0x14
0e 00007fff`e133a4cc : 00000000`00000000 00007fff`e08de090 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0x112
0f 00007fff`f5cc4034 : 00007fff`e1200000 00000000`00000000 00000000`00000000 00000000`00000000 : MSCOREE!CorExeMain_Exported+0x6c
10 00007fff`f8033691 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
11 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21


00 00007fffe07a3181: 00000000e0434352 0006d4a7fe938 0000017b30ad2d48 0000017b2f081690 in the first line: KERNELBASE! The third parameter address 0000017b30ad2d48` in RaiseException+0x68 is our exception class, print it out.

0:000> !do 0000017b30ad2d48
Name:        System.FormatException
MethodTable: 00007fffde285c38
EEClass:     00007fffde3930e0
Size:        160(0xa0) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007fffde2059c0  40002a2        8        System.String  0 instance 0000017b30ad4c80 _className
00007fffde282a50  40002a3       10 ...ection.MethodBase  0 instance 0000000000000000 _exceptionMethod
00007fffde2059c0  40002a4       18        System.String  0 instance 0000000000000000 _exceptionMethodString
00007fffde2059c0  40002a5       20        System.String  0 instance 0000017b30ad2de8 _message
00007fffde2883d8  40002a6       28 ...tions.IDictionary  0 instance 0000000000000000 _data
00007fffde205b70  40002a7       30     System.Exception  0 instance 0000000000000000 _innerException
00007fffde2059c0  40002a8       38        System.String  0 instance 0000000000000000 _helpURL
00007fffde205dd8  40002a9       40        System.Object  0 instance 0000017b30ad2e98 _stackTrace
00007fffde205dd8  40002aa       48        System.Object  0 instance 0000017b30ad2f28 _watsonBuckets
00007fffde2059c0  40002ab       50        System.String  0 instance 0000000000000000 _stackTraceString
00007fffde2059c0  40002ac       58        System.String  0 instance 0000000000000000 _remoteStackTraceString
00007fffde2085a0  40002ad       88         System.Int32  1 instance                0 _remoteStackIndex
00007fffde205dd8  40002ae       60        System.Object  0 instance 0000000000000000 _dynamicMethods
00007fffde2085a0  40002af       8c         System.Int32  1 instance      -2146233033 _HResult
00007fffde2059c0  40002b0       68        System.String  0 instance 0000000000000000 _source
00007fffde2831f8  40002b1       78        System.IntPtr  1 instance                0 _xptrs
00007fffde2085a0  40002b2       90         System.Int32  1 instance       -532462766 _xcode
00007fffde21e720  40002b3       80       System.UIntPtr  1 instance                0 _ipForWatsonBuckets
00007fffde1f5080  40002b4       70 ...ializationManager  0 instance 0000017b30ad2e18 _safeSerializationManager
00007fffde205dd8  40002a1      100        System.Object  0   shared           static s_EDILock
                                 >> Domain:Value  0000017b2efe0af0:NotInit  <<

0:000> !do 0000017b30ad2e98
Name:        System.SByte[]
MethodTable: 00007fffde20dde8
EEClass:     00007fffde390920
Size:        120(0x78) bytes
Array:       Rank 1, Number of elements 96, Type SByte (Print Array)
Content:     .........../{...P.......@..Jm....Z.........................Jm....Y..............................
Fields:
None


At this point _stackTrace is already valid because it has been printed on the Console.

Last but not least, you can use! Threads to find exceptional threads, such as System.FormatException 0000017b30ad2d48 in the following figure, and then use! printexception to print the exceptional objects on this address 0000017b30ad2d48.

0:000> !threads
ThreadCount:      2
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                                                        Lock  
       ID OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
   0    1  80c 0000016816f508f0    2a020 Preemptive  0000016818CCE3B8:0000016818CCFFD0 0000016816ef0b10 0     MTA System.FormatException 0000017b30ad2d48
   6    2 12d8 0000016816f7b0e0    2b220 Preemptive  0000000000000000:0000000000000000 0000016816ef0b10 0     MTA (Finalizer) 


0:000> !printexception 0000017b30ad2d48
Exception object: 0000017b30ad2d48
Exception type:   System.FormatException
Message:          Your format is wrong!!!
InnerException:   <none>
StackTrace (generated):
    SP               IP               Function
    0000001F8F7FEE90 00007FFF811E0951 ConsoleApp4!ConsoleApp4.Program.Run()+0x71
    0000001F8F7FEEE0 00007FFF811E08B9 ConsoleApp4!ConsoleApp4.Program.Main(System.String[])+0x29

StackTraceString: <none>
HResult: 80131537

Three: Summary

Don't treat exceptions as business logic. The overhead may be too much for you. Leave those situations that are truly unexpected, such as TimeoutException.

If you have more questions to interact with me, scan below to enter ~

Tags: C# less Windows

Posted on Sun, 10 May 2020 16:51:15 -0700 by keyser soze