Performance differences between exception handling, dynamic memory requests, and different compilers

Keywords: C++ Linux C

Continue with the previous section Exception handling in c++ ...

Catalog

  1. What happens when an exception is thrown in the main() function

  2. What happens when an exception is thrown in a destructor

  3. Exception specification of functions

    4. Analysis of Dynamic Memory Request Results

   5. New usage of new keywords

1. What happens when an exception is thrown in the main() function

From the previous section Logical analysis of throw exception  You know, when an exception is thrown, it propagates up along the function call stack, during which time, if the exception is caught, the program will run normally; if the exception is still not caught in the main() function, that is, what happens when an exception is thrown in the main() function?(The program crashes, but the results vary slightly depending on the compiler)

 1 #include <iostream>
 2 #include <cstdlib>
 3 
 4 using namespace std;
 5 
 6 class Test
 7 {
 8 public:
 9     Test()
10     {
11         cout << "Test()" << endl;
12     }
13     
14     ~Test()
15     {
16         cout << "~Test()" << endl;
17     }
18 };
19 
20 int main()
21 {
22     cout << "main() begin..." << endl;
23 
24     static Test t;
25     
26     throw 1;  
27               
28     cout << "main() end..." << endl;
29           
30     return 0;
31 }
Throw an exception in main() function

Running the above code on different compilers will result in different results.

Run under g++ with the following results:

main() begin...

Test()

terminate called after throwing an instance of 'int'

Aborted (core dumped)

Run under vs2013 with the following results:

  main() begin...

  Test()

- Pop up exception debugging dialog

From the run result, a global terminate() end function is called after an exception is thrown in main(), which is handled differently by different compilers in the terminal() function.

c++ supports custom end functions. By calling the set_terminate() function to set a custom end function, the system default end() function will become invalid.

(1) The characteristics of the custom end function: as with the default terminal() end function prototype, no parameters and no return value;

Notes on using custom end functions:

(1) You cannot throw an exception in this function again, this is the last chance to handle the exception;

2) The current program must be terminated in some way, such as exit(1), abort();

exit(): Ends the current program and ensures that all global and static local objects are properly destructed;

abort(): terminates a program abnormally and does not call the destructor of any object when terminated abnormally;

(2) The characteristics of the set_terminate() function: 1) the parameter type is the function pointer void(*)(); 2) the return value is the custom terminate() function entry address;

 1 #include <iostream>
 2 #include <cstdlib>
 3 
 4 using namespace std;
 5 
 6 class Test
 7 {
 8 public:
 9     Test()
10     {
11         cout << "Test()" << endl;
12     }
13     
14     ~Test()
15     {
16         cout << "~Test()" << endl;
17     }
18 };
19 
20 void mterminate()
21 {
22     cout << "void mterminate()" << endl;
23     abort();   // Abnormal termination of a program without destructing any objects
24     //exit(1); // Ends the current program, but destructs all global and static local objects
25 }
26 
27 int main()
28 {
29     terminate_handler f = set_terminate(mterminate);
30     
31     cout << "terminate() Entry address of function = " << f << "::" << mterminate << endl;
32     
33     cout << "main() begin..." << endl;
34  
35     static Test t;  
36     
37     throw 1;  
38               
39     cout << "main() end..." << endl;
40           
41     return 0;
42 }
43 /**
44  * The result of running the program at the end of exit(1):
45  * terminate() Entry address of function = 1::1
46  * main() begin...
47  * Test()
48  * void mterminate()
49  * ~Test()
50  */
51 
52 /**
53  * The result of running the program at the end of abort():
54  * terminate() Function's entry address= 1::1, Why are global functions all addresses 1?"
55  * main() begin...
56  * Test()
57  * void mterminate()
58  * Aborted (core dumped)
59  */
Custom End Function Test Case

2. What happens when an exception is thrown in a destructor

In general, destroying a resource used in a destructor, if an exception is thrown in the process of destroying the resource, will result in the resource being used not being completely destroyed; if you dig deeper into this explanation, what happens?

Imagine that the program throws an exception in the main() function, but the exception was not caught, then the exception triggers the system default end function terminal(); because different compilers have different internal implementations of the terminal() function.

(1) If the terminal() function terminates the program in exit(1), then it is possible to call the destructor, at which point an exception is thrown in the destructor, which results in a second call to the terminal() function with unimaginable consequences (similar to a second release of heap space), but powerfulWindows, Linux systems will help us solve this problem, but some embedded operating systems may cause problems.

(2) If the terminal() function ends the program in abort(), the situation in (1) will not occur, which is why the g++ compiler does this.

Note: The terminal() end function is a function that handles the last exception, so it is not possible to throw an exception again in this function, and (1) this rule is violated;

If an exception is thrown in the terminal() end function, it will result in a second call to the terminal() end function.

Conclusion: When an exception is thrown in a destructor, terminate() can be dangerous if it terminates the program by exit(), and it is possible to call the terminate() function twice or even dead-loop.

 1 #include <iostream>
 2 #include <cstdlib>
 3 
 4 using namespace std;
 5 
 6 class Test
 7 {
 8 public:
 9     Test()
10     {
11         cout << "Test()" << endl;
12     }
13     
14     ~Test()
15     {
16         cout << "~Test()" << endl;
17 
18         throw 1// Code Analysis: Called twice mterminate()
19     }
20 };
21 
22 void mterminate()
23 {
24     cout << "void mterminate()" << endl;
25     exit(1); // Ends the current program, but destructs all global and static local objects
26 }
27 
28 int main()
29 {
30     set_terminate(mterminate);
31 
32     cout << "main() begin..." << endl;
33 
34     static Test t;  
35     
36     throw 1;  
37               
38     cout << "main() end..." << endl;
39           
40     return 0;
41 }
Throw exception case test in destructor

Running the above code on different compilers will result in different results.

Run under g++ with the following results:

main() begin...

Test()

void mterminate()      // The first time an exception is thrown in the main() function, call the custom end function mterminate()

~Test()          // At the end of the exit(1) program, a destructor is called, an exception is thrown again in the destructor, and the abort() function is called

Aborted (core dumped)    // Note: Some older compilers may call the custom end function mterminate(), which displays void mterminate()

Run under vs2013 with the following results:

main() begin...

Test()

void mterminate()

At the end of the // exit(1) program, a destructor is called, an exception is thrown in the destructor again, and the exception debugging dialog box appears

Pop up the exception debugging dialog box. //Note: Some older compilers may call the custom end function mterminate(), which displays void mterminate()

Conclusion: The new version of the compiler optimizes the behavior of throwing exceptions in destructors and directly terminates the program abnormally.

3. Exception specification of functions

There may be many ways to determine if a function throws an exception, such as checking the implementation of a function (unfortunately, a third-party library does not provide a function implementation), checking the technical documentation (which may be inconsistent with the version of the function currently in use), but the methods just listed will all have flaws.In fact, there is a simpler and more efficient way to judge whether this function will throw an exception directly from the exception declaration, referred to as the exception specification of the function.

Exception declarations are written after the parameter list as modifiers for function declarations;

1 /* Any exceptions may be thrown */
2 void func1();
3 
4 /* Exception types that can only be thrown: char and int */
5 void func2() throw(char, int);
6 
7 /* Do not throw any exceptions */
8 void func3() throw();

Significance of exception specification:

(1) Prompt the function caller to be prepared for exception handling; (If you want to know what types of exceptions the called function throws, just open the header file to see how the function is declared;)

(2) Prompt the maintainer of the function not to throw other exceptions;

(3) Exception specification is part of the function interface; (Used to illustrate how this function is used correctly;)

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 void func() throw(int)
 6 {
 7     cout << "func()" << endl;
 8     
 9     throw 'c';
10 }
11 
12 int main()
13 {
14     try 
15     {
16         func();
17     } 
18     catch(int) 
19     {
20         cout << "catch(int)" << endl;
21     } 
22     catch(char) 
23     {
24         cout << "catch(char)" << endl;
25     }
26 
27     return 0;
28 }
Exception Test Cases Beyond Exception Specifications

Running the above code on different compilers will result in different results.

Run under g++ with the following results:

func()

terminate called after throwing an instance of 'char'

Aborted (core dumped)

Run under vs2013 with the following results:

func()

catch(char)  // The exception was caught, indicating that it is not restricted by the exception specification

By re-examining the results of the above code, we find that in g+, when an exception is not in the specification of a function exception, a global function, unexpected(), is called, in which the default global end function terminate(), is called again.

However, in vs2013, exceptions are not limited by the specification of function exceptions.

Conclusion: The g++ compiler follows the c++ specification, but the vs2013 compiler is not restricted by this constraint.

Tip: Function exception specifications are handled differently by different compilers, so it is necessary to test the compiler currently in use when developing a project.

Custom exception functions are supported in c++; by calling set_unexpected() functions to set custom exception functions, the system default global function unexpected() will become invalid;

(1) Features of custom exception functions: Like the default global function unexpected() prototype, no parameters and no return values;

(2) Notes on using custom exception functions:

(You can throw an exception in a function (resume program execution when the exception meets the exception specification of the trigger function; otherwise, call the global terminate() function to end the program);

(3) The features of the set_unexpected() function: 1) the parameter type is the function pointer void(*)(); 2) the return value is the custom unexpected() function entry address;

 1 #include <iostream>
 2 #include <cstdlib>
 3 
 4 using namespace std;
 5 
 6 void m_unexpected()
 7 {
 8     cout << "void m_unexpected()" << endl;
 9   
10     throw 1;  // 2 This exception meets the exception specification and can be caught
11     // terminate(); // If so, the result will be the same as that of the previous program
12 }
13 
14 void func() throw(int)
15 {
16     cout << "func()" << endl;
17     
18     throw 'c';  // 1 This is called because the exception specification is not met m_unexpected() function
19 }
20 
21 int main()
22 {
23     set_unexpected(m_unexpected);
24     
25     try 
26     {
27         func();
28     } 
29     catch(int) 
30     {
31         cout << "catch(int)" << endl;
32     } 
33     catch(char) 
34     {
35         cout << "catch(char)" << endl;
36     }
37 
38     return 0;
39 }
Test case for custom unexpected() function

Running the above code on different compilers will result in different results.

Run under g++ with the following results:

func()

void m_unexpected()

catch(int)  // Due to custom exception functionsm_unexpected() Exception thrown in throw 1 The function exception specification is met, so the exception is caught

Run under vs2013 with the following results:

func()

catch(char)  // vs2013 does not follow the c++ specification and is not restricted by exception specification. It catches throw'c'exception in function exception specification directly

Conclusion: (g++) unexpected() function is the last chance to handle the exception correctly. If it is not caught, terminate() function will be called and the current program will end up with an exception.

(vs2013) There are no restrictions on the specification of function exceptions, and all functions can throw any exceptions.

4. Analysis of Dynamic Memory Request Results

In c language, when a dynamic memory request is made using the malloc function, the corresponding memory header address is returned if it succeeds, and the NULL value is returned if it fails.

In the c++ specification, when sufficient memory space is dynamically requested by overloading the new, new[] operators,

(1) If successful, call the constructor in the acquired space to create the object and return the object address;

(2) Failure (insufficient memory space) may result in different results depending on the compiler;

(1) Return NULL values; (Early compiler behavior, not part of the c++ specification)

(2) throw std::bad_alloc exception; (late compilers throw exceptions, and some early compilers still return NULL values)

Note: Different compilers are also uncertain about how to throw exceptions. The c++ specification throws std::bad_alloc exceptions in the new_handler() function, and the new_handler() function is called automatically when a memory request fails.

When there is not enough memory space, the global new_hander() function is called, which means we have a chance to organize enough memory space; therefore, we can customize the new_hander() function and set the custom new_hander() function through the global function set_new_hander().(experimentally, some compilers do not define global new_hander() functions, such as vs2013, g++, see case 1)

It is important to note that the return value of set_new_hander() is the entry address of the default global new_hander() function.

The return value of the set_terminate() function is the entry address of the custom terminate() function;

The return value of the set_unexpected() function is the entry address of the custom unexpected() function.

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 void my_new_handler()
 6 {
 7     cout << "void my_new_handler()" << endl;
 8 }
 9 
10 int main(int argc, char *argv[])
11 {
12     // If the compiler has a global new_handler() Function, then func != NULL,Otherwise, func == NULL;
13     new_handler func = set_new_handler(my_new_handler);
14 
15     try
16     {
17         cout << "func = " << func << endl;  
18         
19         if( func )
20         {
21             func();
22         }
23     }
24     catch(const bad_alloc&)
25     {
26         cout << "catch(const bad_alloc&)" << endl;
27     }
28     
29     return 0;
30 }
Case 1: Prove if the compiler defines a global new_handler() function

Running the above code on different compilers will result in different results.

Run under vs2013 and g++ with the following results:

func = 0   // => Global new_handler() function is not defined in vs2013 and g++.

Run under BCC with the following results:

func = 00401468

catch(const bad_alloc&)  // The global new_handler() function is defined in the BCC and an std::bad_alloc exception is thrown in the function

 

 1 #include <iostream>
 2 #include <new>
 3 #include <cstdlib>
 4 #include <exception>
 5 
 6 using namespace std;
 7 
 8 class Test
 9 {
10     int m_value;
11 public:
12     Test()
13     {
14         cout << "Test()" << endl;
15         
16         m_value = 0;
17     }
18     
19     ~Test()
20     {
21         cout << "~Test()" << endl;  
22     }
23     
24     void* operator new (size_t size)
25     {
26         cout << "operator new: " << size << endl;
27         
28         return NULL;
29     }
30     
31     void operator delete (void* p)
32     {
33         cout << "operator delete: " << p << endl;
34         
35         free(p);
36     }
37     
38     void* operator new[] (size_t size)
39     {
40         cout << "operator new[]: " << size << endl;
41         
42         return NULL;
43     }
44     
45     void operator delete[] (void* p)
46     {
47         cout << "operator delete[]: " << p << endl;
48         
49         free(p);
50     }
51 };
52 
53 int main(int argc, char *argv[])
54 {
55     Test* pt = new Test();  
56     
57     cout << "pt = " << pt << endl;
58     
59     delete pt;
60     
61     pt = new Test[5];
62     
63     cout << "pt = " << pt << endl;
64     
65     delete[] pt; 
66     
67     return 0;
68 }
Case 2: Performance of different compilers when memory request fails

Running the above code on different compilers will result in different results.

Run under g++ with the following results:

operator new: 4

Test()  // A NULL value is returned due to a heap space request failure, and then an object is created on this failed space. When m_value = 0; is executed (equivalent to assigning a value on an illegal address), the compiler misspells the segment

Segmentation fault (core dumped)

Run under vs2013 with the following results:

operator new: 4

pt = 00000000

operator new[]: 24

pt = 00000000

Run under BCC with the following results:

operator new: 4

pt = 00000000

operator new[]: 24

pt = 00000000

operator delete[]: 00000000

Summary: In the g++ compiler, memory space requests fail and constructors continue to be called to create objects, which can result in segment errors; in the vs2013, BCC compilers, memory space requests fail, returning NULL directly.

In order to unify the behavior of different compilers in memory requests, it is necessary to overload the new, delete, or new[], delete[] operators, and return the NULL value directly when the memory request fails instead of throwing the std::bad_alloc exception, which must decorate the memory request function with throw().

 1 #include <iostream>
 2 #include <new>
 3 #include <cstdlib>
 4 #include <exception>
 5 
 6 using namespace std;
 7 
 8 class Test
 9 {
10     int m_value;
11 public:
12     Test()
13     {
14         cout << "Test()" << endl;
15         
16         m_value = 0;
17     }
18     
19     ~Test()
20     {
21         cout << "~Test()" << endl;  
22     }
23     
24     void* operator new (size_t size) throw()
25     {
26         cout << "operator new: " << size << endl;
27         
28         return NULL;
29     }
30     
31     void operator delete (void* p)
32     {
33         cout << "operator delete: " << p << endl;
34         
35         free(p);
36     }
37     
38     void* operator new[] (size_t size) throw()
39     {
40         cout << "operator new[]: " << size << endl;
41         
42         return NULL;
43     }
44     
45     void operator delete[] (void* p)
46     {
47         cout << "operator delete[]: " << p << endl;
48         
49         free(p);
50     }
51 };
52 
53 int main(int argc, char *argv[])
54 {
55     Test* pt = new Test();
56     
57     cout << "pt = " << pt << endl;
58     
59     delete pt;
60     
61     pt = new Test[5];
62     
63     cout << "pt = " << pt << endl;
64     
65     delete[] pt; 
66     
67     return 0;
68 }
Case 3: (Optimize) Performance of different compilers when memory requests fail

The results of the tests for g++, vs2013 and BCC compilers are the same, and the output is as follows:

operator new: 4

pt = 00000000

operator new[]: 24

pt = 00000000

5. New usage of new keywords

(1) nothrow keyword

 1 #include <iostream>
 2 #include <exception>
 3 
 4 using namespace std;
 5 
 6 void func1()
 7 {
 8     try
 9     {
10         int* p = new(nothrow) int[-1]; 
11         
12         cout << p << endl;
13         
14         delete[] p; 
15     }
16     catch(const bad_alloc&)
17     {
18         cout << "catch(const bad_alloc&)" << endl;
19     }    
20     
21     cout << "--------------------" << endl;
22     
23     try
24     {
25         int* p = new int[-1];
26         
27         cout << p << endl;
28         
29         delete[] p; 
30     }
31     catch(const bad_alloc&)
32     {
33         cout << "catch(const bad_alloc&)" << endl;
34     }    
35 }
36 
37 int main(int argc, char *argv[])
38 {
39     func1();
40     
41     return 0;
42 }
Use of nothrow keyword

Running the above code on different compilers will result in different results.

Run under g++, BCC with the following results:

0 // The nothrow keyword is used to return NULL directly when a dynamic memory request fails
--------------------
Catch(const bad_alloc &) //No nothrow keyword throws std::bad_alloc exception when dynamic memory request fails

Compilation failed under vs2013:

The reason is that the memory request is too large, that is, the total size of the array must not exceed 0x7fffffff bytes;

Conclusion: The function of the nothrow keyword: Do not throw exceptions regardless of the result of a dynamic memory request, but there will be differences between compilers.

(2) Create objects at specified addresses through new

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 void func2()
 6 {
 7     int bb[2] = {0};
 8     
 9     struct ST
10     {
11         int x;
12         int y;
13     };
14     
15     // adopt new Create an object at a specified address
16     // Will dynamic memory ST Create on stack space ( int bb[2] = {0}),But to make sure they have the same memory model, here is 8 bytes
17     ST* pt = new(bb) ST();  
18     
19     pt->x = 1;
20     pt->y = 2;
21     
22     cout << bb[0] << "::" << bb[1] << endl;
23     
24     bb[0] = 3;
25     bb[1] = 4;
26     
27     cout << pt->x << "::" << pt->y << endl;
28     
29     pt->~ST();  // Since the space in which the object is created is specified, the call destructor must be displayed
30 }
31 
32 int main(int argc, char *argv[])
33 {   
34     func2();
35     
36     return 0;
37 }
Create an object on a specified address with new

Run under g++, vs2013, BCC with the following results:

1::2

3::4

The conclusion of dynamic memory requests:

(1) Different compilers have different implementation details on dynamic memory allocation;

(2) The compiler may redefine the implementation of new and throw a bad_alloc exception in the implementation; (vs2013, g++)

(3) The global new_handler() function may not be set in the default implementation of the compiler; (vs2013, g++)

(4) For code with high portability requirements, consider the specific details of new;

We can further validate the above conclusion by taking vs2013 as an example and finding the new.cpp and new2.cpp files (file path: C:\Program Files (x86)Microsoft Visual Studio 12.0VCcrtsrc) in the compiler's installation package. Analyzing their source code discovery, we will call the _newcallh(cb) function when the memory request fails, which can be viewed as follows: https://docs.microsoft.com/zh-cn/cpp/c-runtime-library/reference/callnewh?view=vs-2015

  

   

In vs, when a dynamic memory request fails, an std::bad_alloc exception is thrown instead of returning a NULL value.

 1 #ifdef _SYSCRT
 2 #include <cruntime.h>
 3 #include <crtdbg.h>
 4 #include <malloc.h>
 5 #include <new.h>
 6 #include <stdlib.h>
 7 #include <winheap.h>
 8 #include <rtcsup.h>
 9 #include <internal.h>
10 
11 // Two versions of new Implementation, thrown when failed bad_alloc abnormal
12 void * operator new( size_t cb )
13 {
14     void *res;
15 
16     for (;;) {
17 
18         //  allocate memory block
19         res = _heap_alloc(cb);
20 
21         //  if successful allocation, return pointer to memory
22 
23         if (res)
24             break;
25 
26         //  call installed new handler
27         if (!_callnewh(cb))  // If the application fails, it is thrown bad_alloc abnormal
28             break;
29 
30         //  new handler was successful -- try to allocate again
31     }
32 
33     RTCCALLBACK(_RTC_Allocate_hook, (res, cb, 0));
34 
35     return res;
36 }
37 #else  /* _SYSCRT */
38 
39 #include <cstdlib>
40 #include <new>
41 
42 _C_LIB_DECL
43 int __cdecl _callnewh(size_t size) _THROW1(_STD bad_alloc);
44 _END_C_LIB_DECL
45 
46 void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
47         {       // try to allocate size bytes
48         void *p;
49         while ((p = malloc(size)) == 0)
50                 if (_callnewh(size) == 0)
51                 {       // report no memory
52                         _THROW_NCEE(_XSTD bad_alloc, );
53                 }
54 
55         return (p);
56         }
Source analysis new.cpp
 1 #include <cruntime.h>
 2 #include <malloc.h>
 3 #include <new.h>
 4 #include <stdlib.h>
 5 #include <winheap.h>
 6 #include <rtcsup.h>
 7 
 8 void *__CRTDECL operator new(size_t) /*_THROW1(std::bad_alloc)*/;
 9 
10 void * operator new[]( size_t cb )
11 {
12     void *res = operator new(cb);
13 
14     RTCCALLBACK(_RTC_Allocate_hook, (res, cb, 0));
15 
16     return res;
17 }
Source analysis new2.cpp

Posted by thelinx on Sun, 22 Mar 2020 22:11:49 -0700