Does How You Access Class Properties Make A Difference?
One day I found myself writing methods for Forex Trading bots where numerous methods within the bot had to access framework classes with properties that were read only.
I started writing code accessing the properties one way, when I was like, “wait a minute. I can do this a few different ways, but which way is the fastest?” This is an important question because when it comes to writing code that trades on the Forex markets, especially from VPS Providers, every nano-second counts.
That’s where this Curious Consultant started wondering… does how you access class properties make a difference in C# performance? If so, how much?
Let’s benchmark it and find out!
Constructing the Test
For this test, there are 4 methods:
- a parent method that would call 3 other test methods a varying number of times to simulate the forex bot calling these methods every time a tick came through.
- the first test method will access to external read-only class and its properties directly
- the second test method will create a local variable that references the external class. In a sense, cache it locally.
- the third test method that will have the external class passed in as a method parameter and then access the properties from the local method parameter
The trick here though is try and ensure between calls that the methods and such wouldn’t stay cached in any CPU registers. So between calls, properties in the external class are updated.
The code is written in Visual Studio 2017 targeting .Net Framework version 4.8 x64, compiled in “Release” mode the build option “Optimize Code” checked. The source code is available at the end of this blog so you can benchmark it on your own system.
The exe file was run under Windows 10 Professional 64-bit with 32GB of memory.
The test was run with each child method called the following number of times:
- 500
- 5,000
- 50,000
- 500,000
- 5,000,000
- 50,000,000
Ready to start!
These tests were run twice after a fresh boot with no other applications runrning to reduce the impact on the CPU.
All times are indicated in minutes.seconds.milliseconds format. Lower numbers indicate faster run time performance.
Winners are highlighted in green; there are no points for second place.
The Results:
Test Run #1
|
Number of Times Test Methods Were Called |
|||||
|
500 |
5000 |
50,000 |
500,000 |
5,000,000 |
50,000,000 |
Test #1 Access Class Directly |
00:00.0002837 |
00:00.0002992 |
00:00.0341171 |
00:00.0304793 |
00:02.9305099 |
04:49.4940290 |
Test #2 Cache Locally |
00:00.0001670 |
00:00.0002742 |
00:00.0267010 |
00:00.0292805 |
00:02.7131260 |
04:27.3231493 |
Test #3 Access Via Method Parameter |
00:00.0001316 |
00:00.0002665 |
00:00.0262321 |
00:00.0289119 |
00:02.7097149 |
04:27.1189106 |
Test Run #2
|
Number of Times Test Methods Were Called |
|||||
|
500 |
5000 |
50,000 |
500,000 |
5,000,000 |
50,000,000 |
Test #1 Access Class Directly |
00:00.0002547 |
00:00.0003162 |
00:00.0292074 |
00:00.0302657 |
00:02.9824971 |
05:18.0097801 |
Test #2 Cache Locally |
00:00.0001457 |
00:00.0002761 |
00:00.0260667 |
00:00.0273395 |
00:02.7396041 |
04:50.9388479 |
Test #3 Access Via Method Parameter |
00:00.0001318 |
00:00.0002748 |
00:00.0260007 |
00:00.0273393 |
00:02.8781378 |
04:53.5245467 |
In Summary:
Does How You Access Class Properties Make A Difference? Judging from the results, it certainly does! Not be huge leaps and bounds, but definitely enough that it could make the difference between a profit or loss when you need to trade at the speed of the stock or forex markets.
As the number of times the test methods were called, the difference between referencing the class properties directly versus either caching or being passed as a method parameter definitely became more noticeable. In both tests, where the C# class was passed as a method parameter, it won hands down for the first 4 runs of 500 through to 500,000; afterwards, it was 50-50 between being passed as a parameter or cached locally.
So in the more practical world, taking into account at the top end of the test the methods were called 50,000,000 times, the speed difference will probably be negligible to the end user. In fact, they’ll probably blame it on their computer’s internet connection or their operating system’s lack of responsiveness.
As for this Curious Consultant, when speed is definitely needed, it seems best to pass as a method parameter to run the fastest over 90% of the time.
The Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
using System; using System.Collections.Generic; using System.Collections; using System.Collections.Concurrent; using System.Data; using System.Data.Sql; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Threading; using System.Diagnostics; namespace TestApplication { class Program { static void Main(string[] args) { DateTime end; DateTime start = DateTime.Now; Console.WriteLine("### Overall Start Time: " + start.ToLongTimeString()); Console.WriteLine(); TestForTestClassForAccessPerformance(500); _tc.a = 4; _tc.d = 0.1; _tc.s = "Test"; //Force changes in values so class doesn't stay cached anywhere TestForTestClassForAccessPerformance(5000); _tc.a = 2; _tc.d = 0.11; _tc.s = "Test1"; TestForTestClassForAccessPerformance(50000); _tc.a = 3; _tc.d = 0.31; _tc.s = "Test2"; TestForTestClassForAccessPerformance(500000); _tc.a = 4; _tc.d = 0.12; _tc.s = "Test3"; TestForTestClassForAccessPerformance(5000000); _tc.a = 2; _tc.d = 0.11; _tc.s = "Test4"; TestForTestClassForAccessPerformance(50000000); end = DateTime.Now; Console.WriteLine(); Console.WriteLine("### Overall End Time: " + end.ToLongTimeString()); Console.WriteLine("### Overall Run Time: " + (end - start)); Console.WriteLine(); Console.WriteLine("Hit Enter to Exit"); Console.ReadLine(); } //#################################################### public class TestClassForAccessPerformance { public int a; public bool b; public double d; public string s; public Guid g; public List<bool> lb; public TestClassForAccessPerformance() { a = 3; b = false; d = 0.13; s = "Hello Test World!"; g = Guid.NewGuid(); lb = new List<bool>(13); } } public static TestClassForAccessPerformance _tc = new TestClassForAccessPerformance(); static void TestForTestClassForAccessPerformance(int numberOfLoops) { //Tests accessing the class property directly Console.WriteLine("######## " + System.Reflection.MethodBase.GetCurrentMethod().Name); Console.WriteLine("Number of loops: " + numberOfLoops.ToString("#,##0")); Stopwatch sw = new Stopwatch(); DateTime end = DateTime.Now; DateTime start = DateTime.Now; Thread.Sleep(500); Console.WriteLine("###########################################################"); Console.WriteLine("Starting Test1ForTestClassForAccessPerformance test with " + numberOfLoops.ToString("#,##0") + " at: " + DateTime.Now.ToLongTimeString()); sw.Restart(); for (int x = 0; x < numberOfLoops; x++) { Test1ForTestClassForAccessPerformance(numberOfLoops); } sw.Stop(); Console.WriteLine("Finished at: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("Time to run: " + sw.Elapsed.ToString("mm\\:ss\\.fffffff")); Console.WriteLine(); Console.WriteLine("###########################################################"); Console.WriteLine("Starting Test2ForTestClassForAccessPerformance test with " + numberOfLoops.ToString("#,##0") + " at: " + DateTime.Now.ToLongTimeString()); sw.Restart(); for (int x = 0; x < numberOfLoops; x++) { Test2ForTestClassForAccessPerformance(numberOfLoops); } sw.Stop(); Console.WriteLine("Finished at: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("Time to run: " + sw.Elapsed.ToString("mm\\:ss\\.fffffff")); Console.WriteLine(); Console.WriteLine("###########################################################"); Console.WriteLine("Starting Test3ForTestClassForAccessPerformance test with " + numberOfLoops.ToString("#,##0") + " at: " + DateTime.Now.ToLongTimeString()); sw.Restart(); for (int x = 0; x < numberOfLoops; x++) { Test3ForTestClassForAccessPerformance(numberOfLoops, _tc); } sw.Stop(); Console.WriteLine("Finished at: " + DateTime.Now.ToLongTimeString()); Console.WriteLine("Time to run: " + sw.Elapsed.ToString("mm\\:ss\\.fffffff")); Console.WriteLine(); } //########################################################### static void Test1ForTestClassForAccessPerformance(int numberOfLoops) { //Tests accessing the class property directly double totalSum = 0; numberOfLoops /= (numberOfLoops > 100000 ? 10000 : 100); for (int x = 0; x < numberOfLoops; x++) { if (x % 5 != 0) totalSum += (_tc.a * _tc.d) / x + (Math.PI * _tc.d); } } //########################################################### static void Test2ForTestClassForAccessPerformance(int numberOfLoops) { //cache it locally to see what happens TestClassForAccessPerformance tc = _tc; double totalSum = 0; numberOfLoops /= (numberOfLoops > 100000 ? 10000 : 100); for (int x = 0; x < numberOfLoops; x++) { if (x % 5 != 0) totalSum += (tc.a * tc.d) / x + (Math.PI * tc.d); } } //########################################################### static void Test3ForTestClassForAccessPerformance(int numberOfLoops, TestClassForAccessPerformance tc) { //Tests accessing the class property when the class is passed as a paramneter double totalSum = 0; numberOfLoops /= (numberOfLoops > 100000 ? 10000 : 100); for (int x = 0; x < numberOfLoops; x++) { if (x % 5 != 0) totalSum += (tc.a * tc.d) / x + (Math.PI * tc.d); } } } //class } //namespace |