Increase Performance with an Object Pool or Why Singleton May Cause Performance Issues

Introduction

 
It frequently occurs that in order to improve memory management and to lessen the number of objects being created (especially those objects that are making over-the-network calls), the singleton pattern is used.
 
Often it is justified by the concern that some sloppy code may not be releasing the objects properly or in-time for the Garbage Collector to keep the application memory low.
 
So, one of those 'over-the-network' objects is a WCF Service Client (or Web Reference instance).
 
In this article, I would like to demonstrate that
  1. Using a Singleton client to access the WCF Web Service has some serious performance issues.
  2. Creating a new Client object from the Web Reference every time you need to call a WCF Service performs much faster than a Singleton Client.
  3. Using an object pool (object pooling) is as speedy as option two, but also uses less application memory.
Please refer to the attached code for references. Both WCF web service and a test client are included.
 
Let's go over the projects quickly, how and why they were created.
 
First, this is how a WCF web service Application is created. 
 
 
I named my Project 'WcfDataService'.
 
 
You'll see that besides standard files like .svc and an Interface for it IDataService, there is also a data access class that actually performs some data manipulations. In this case, it has one insert and one select method Implemented in class "Dal" in Dal.cs. This is to make a service more realistic, which would in turn, make a Testing console app somewhat real as well.
 
Dal.cs
  1. public class Dal  
  2.    {  
  3.        const string connectionString = "data source=localhost\\dev400;Initial Catalog=World;User id=sa; Password=xyz";  
  4.        public DataSet QeuryProduct()  
  5.        {  
  6.            using (SqlConnection cn = new SqlConnection(connectionString))  
  7.            {  
  8.                SqlDataAdapter da = new SqlDataAdapter("Select Top(50) * From [WorldAndCapitals]", cn);  
  9.                DataSet ds = new DataSet();  
  10.                da.Fill(ds);  
  11.                return ds;  
  12.            }  
  13.        }  
  14.   
  15.        public int InsertContryContinent(string continent, string country, string capital, int population, string totalArea)  
  16.        {  
  17.            using (SqlConnection cn = new SqlConnection(connectionString))  
  18.            {  
  19.                SqlCommand cmd = new SqlCommand();  
  20.                cmd.Connection = cn;  
  21.                cmd.CommandText = "INSERT INTO [dbo].[Continents2] ([Continent] ,[Country],[Capital],[Population] ,[Total area])   VALUES(@continent,@country,@capital,@population,@totalArea)";  
  22.   
  23.                cmd.Parameters.AddWithValue("@continent", continent);  
  24.                cmd.Parameters.AddWithValue("@country", country);  
  25.                cmd.Parameters.AddWithValue("@capital", capital);  
  26.                cmd.Parameters.AddWithValue("@population", population);  
  27.                cmd.Parameters.AddWithValue("@totalArea", totalArea);  
  28.                cn.Open();  
  29.                int rowsInserted = cmd.ExecuteNonQuery();  
  30.   
  31.                return rowsInserted;  
  32.            }  
  33.        }  
  34.    } 
DataService.svc.cs
  1. public class DataService : IDataService  
  2.     {         
  3.         public int InsertContryContinent(string continent, string country, string capital, int population, string totalArea)  
  4.         {  
  5.             var dal = new Dal();  
  6.             return dal.InsertContryContinent(continent, country, capital, population, totalArea);  
  7.         }  
  8.   
  9.         public DataSet QueryProduct()  
  10.         {  
  11.             var dal = new Dal();  
  12.             return dal.QeuryProduct();  
  13.         }  
  14.     }  
Next, I created an IIS website to publish the service. Basically, I used a Project folder as a directory path of the new web site and port number chose 8055.
 
The web.config is:
  1. <?xml version="1.0"?>  
  2. <configuration>  
  3.   <appSettings>  
  4.     <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true"/>  
  5.   </appSettings>    
  6.   <system.web>  
  7.     <compilation targetFramework="4.7.2" debug="true"/>  
  8.     <httpRuntime targetFramework="4.7.2"/>  
  9.   </system.web>  
  10.   <system.serviceModel>  
  11.     <services>  
  12.       <service behaviorConfiguration="Default2" name="WcfDataService.DataService">  
  13.         <endpoint address="WcfDataService" binding="basicHttpBinding" contract="WcfDataService.IDataService"/>  
  14.         <host>  
  15.           <baseAddresses>  
  16.             <add baseAddress="http://localhost"/>  
  17.           </baseAddresses>  
  18.         </host>  
  19.       </service>  
  20.     </services>  
  21.     <behaviors>  
  22.       <serviceBehaviors>  
  23.         <behavior name="Default2">  
  24.           <serviceMetadata httpGetEnabled="true"/>  
  25.         </behavior>  
  26.       </serviceBehaviors>  
  27.     </behaviors>  
  28.   </system.serviceModel>  
  29. </configuration> 
Navigate to the newly created web site:
 
 
 
Next, I created a testing project called WcfPoolTest. It's just a Windows Console application.
 
I used a .wsdl link from the previous picture to create a reference to the service DataServiceReference from the project called WcfPoolTest.
 
 
The test is basically doing an insert operation of 1000 records. There are 10 threads. Each thread inserts 100 times.
 
There are 3 test routines:  Test Routine Simple, Test Routine Singleton, Test Routine Pooled (implemented by TestRoutineSimpleMultiple, TestRoutineSingletonMultiple, TestRoutinePooledMultiple classes).
 
Program.cs
  1. class Program  
  2.     {  
  3.         static void Main(string[] args)  
  4.         {  
  5.             var seconds = TestSimpleMultiple();  
  6.             //var seconds = TestSingletonMultiple();  
  7.             //var seconds = TestObjectPoolMultiple();  
  8.               
  9.             Console.WriteLine("elapsed {0}", seconds);  
  10.             Console.Read();  
  11.         }  
  12.   
  13.         static double TestObjectPoolMultiple()  
  14.         {  
  15.             ITestRoutine testRoutine = new TestRoutinePooledMultiple();  
  16.             StopwatchTest stopwatchTest = new StopwatchTest();  
  17.             return stopwatchTest.RunStopwatch(testRoutine.RunMultithreaded);  
  18.         }  
  19.         static double TestSingletonMultiple()  
  20.         {  
  21.             ITestRoutine testRoutine = new TestRoutineSingletonMultiple();  
  22.             StopwatchTest stopwatchTest = new StopwatchTest();  
  23.             return stopwatchTest.RunStopwatch(testRoutine.RunMultithreaded);  
  24.         }  
  25.         static double TestSimpleMultiple()  
  26.         {  
  27.             ITestRoutine testRoutine = new TestRoutineSimpleMultiple();  
  28.             StopwatchTest stopwatchTest = new StopwatchTest();  
  29.             return stopwatchTest.RunStopwatch(testRoutine.RunMultithreaded);  
  30.         }  
  31.     } 
The first test TestSimpleMultiple() is in the case when the web reference object is created just before each call is submitted. A new WCF Client is created each time.

TestRoutineSimpleMultiple.cs
  1. public class TestRoutineSimpleMultiple : ITestRoutine  
  2.     {          
  3.         public void RunMultithreaded()  
  4.         {  
  5.             var result = Parallel.For(1, 11, (i) => RunMultiple100());  
  6.         }  
  7.   
  8.         private void RunMultiple100()  
  9.         {  
  10.             int Population = 120000000;  
  11.             for (int i = 0; i<100; i++)  
  12.             {  
  13.                 using (DataServiceReference.DataServiceClient client = new DataServiceReference.DataServiceClient())  
  14.                 {  
  15.                     Population++;  
  16.                     client.InsertContryContinent("Asia""China1""Pekin", Population, "555000 km2");  
  17.                 }  
  18.             }  
  19.         }  
Next, Singleton is similar, only the single instance is used via WebClientSingleton.GetClentInstance()
 
TestRoutineSingleton.cs
  1. public class TestRoutineSingletonMultiple : ITestRoutine  
  2.     {          
  3.         public void RunMultithreaded()  
  4.         {  
  5.             var result = Parallel.For(1, 11, (i) => RunMultiple100());  
  6.         }  
  7.   
  8.         private void RunMultiple100()  
  9.         {  
  10.             int Population = 120000000;  
  11.             for (int i = 0; i < 100; i++)  
  12.             {  
  13.                 Population++;  
  14.                 var cs = WebClientSingleton.GetClentInstance();  
  15.                 cs.InsertContryContinent("Asia""China1""Pekin", Population, "555000 km2");  
  16.             }  
  17.         } 
Next, TestRoutinePooledMultiple is very similar on the surface to TestRoutineSingletonMultiple implementation. The only difference is that WebClientPooled is used instead.
 
TestRoutinePooledMultiple.cs
  1. public class TestRoutinePooledMultiple : ITestRoutine  
  2.     {          
  3.         public void RunMultithreaded()  
  4.         {  
  5.             var result = Parallel.For(1, 11, (i) => RunMultiple100());  
  6.         }  
  7.   
  8.         private void RunMultiple100()  
  9.         {  
  10.             int Population = 120000000;  
  11.             for (int i = 0; i < 100; i++)  
  12.             {  
  13.                 Population++;  
  14.                 var cs = WebClientPooled.GetClentInstance();  
  15.                 cs.InsertContryContinent("Asia""China1""Pekin", Population, "555000 km2");  
  16.             }  
  17.         } 
Next, the singleton implementation of WebClientSingleton. It is very simple. Notice that lock is used against multithreading.
 
WebClientSingleton.cs
  1. public class WebClientSingleton  
  2.     {  
  3.         public static DataServiceReference.DataServiceClient client;  
  4.         public static object classLock = new object();  
  5.         private WebClientSingleton()  
  6.         { }  
  7.   
  8.         public static DataServiceReference.DataServiceClient GetClentInstance()  
  9.         {  
  10.             if (client == null)  
  11.             {    
  12.                 lock (classLock)//multithreading will create multiple instances if not locking and checking again.  
  13.                 {  
  14.                     if (client == null)  
  15.                         client = new DataServiceReference.DataServiceClient();  
  16.                 }  
  17.             }  
  18.             return client;  
  19.         }  
  20.     } 
 And the last is a WebClientPooled implementation. It's kind of similar to the Singleton since the pool is stored in a static variable. But for each request, the next object is returned by its order in the array (round-robin).
 
WebClientPooled.cs
  1. public class WebClientPooled  
  2.    {  
  3.        private static int poolSize = 10;  
  4.        private static int index = -1;  
  5.        private static DataServiceReference.DataServiceClient[] clients = new DataServiceReference.DataServiceClient[poolSize];  
  6.          
  7.        public static object classLock = new object();  
  8.        private WebClientPooled()  
  9.        { }  
  10.   
  11.        public static DataServiceReference.DataServiceClient GetClentInstance()  
  12.        {  
  13.            lock (classLock)  
  14.            {  
  15.                GetNextIndex();  
  16.                if (clients[index] == null)  
  17.                {  
  18.                    clients[index] = new DataServiceReference.DataServiceClient();  
  19.                }  
  20.   
  21.                return clients[index];  
  22.            }  
  23.        }          
  24.        static void GetNextIndex()  
  25.        { 
  26.            if (index == poolSize - 1) index = -1;
  27.            index = (index + 1)%poolSize;  
  28.        }  
  29.    } 
So, the test itself:
  1. static void Main(string[] args)  
  2.         {  
  3.             var seconds = TestSimpleMultiple();  
  4.             //var seconds = TestSingletonMultiple();  
  5.             //var seconds = TestObjectPoolMultiple();  
  6.               
  7.             Console.WriteLine("elapsed {0}", seconds);  
  8.             Console.Read();  
  9.         } 
 And the results,
 
 TestSimpleMultiple():  runs for an average 4.2 seconds, memory: 27 MB.      => one of the best in execution speed. worst in memory usage.
 TestSingletonMultiple():  runs for an average 14.2 seconds, memory: 25 MB. =>one of the best in memory. worst in execution speed.
 TestObjectPoolMultiple(): runs for an average 4.2 seconds, memory: 25 MB. => best in both, memory and execution speed.
 
There is a dramatic degradation in execution speed by Singleton Client.
 
There is about a 4 % increase in memory usage by Simple(generic) Client.
 
There is the best of both worlds when running Pooled Client.
 

Summary

 
When there is a performance issue related to the WCF Service, somehow the first impulse is to blame the service (i.e. Microsoft. After all, the multi-thread applications are being slowed down by the WCF web service. But the issue could be in the middle-man so to say. In this case, it's the way WCF Service Client is being used/instanciated.