Reduce allocations when setting AsyncLocal values
authorEric Eilebrecht <ericeil@microsoft.com>
Fri, 25 Sep 2015 21:03:08 +0000 (14:03 -0700)
committerEric Eilebrecht <ericeil@microsoft.com>
Sat, 26 Sep 2015 17:49:50 +0000 (10:49 -0700)
1: Allocate a new AsyncLocal change notification list only if we need to add an item to the list.
2: Pre-size any collections we allocate, and manually copy into them to avoid IEnumerator allocations, etc.
3: Store the change notification list in an array, rather than a List, to avoid the extra List allocation.

src/mscorlib/src/System/Threading/ExecutionContext.cs

index 3fc1e14..9430811 100644 (file)
@@ -59,18 +59,18 @@ namespace System.Threading
         static ExecutionContext t_currentMaybeNull;
 
         private readonly Dictionary<IAsyncLocal, object> m_localValues;
-        private readonly List<IAsyncLocal> m_localChangeNotifications;
+        private readonly IAsyncLocal[] m_localChangeNotifications;
 
         private ExecutionContext()
         {
             m_localValues = new Dictionary<IAsyncLocal, object>();
-            m_localChangeNotifications = new List<IAsyncLocal>();
+            m_localChangeNotifications = Array.Empty<IAsyncLocal>();
         }
 
-        private ExecutionContext(ExecutionContext other)
+        private ExecutionContext(Dictionary<IAsyncLocal, object> localValues, IAsyncLocal[] localChangeNotifications)
         {
-            m_localValues = new Dictionary<IAsyncLocal, object>(other.m_localValues);
-            m_localChangeNotifications = new List<IAsyncLocal>(other.m_localChangeNotifications);
+            m_localValues = localValues;
+            m_localChangeNotifications = localChangeNotifications;
         }
 
         [SecuritySafeCritical]
@@ -191,18 +191,39 @@ namespace System.Threading
             if (previousValue == newValue)
                 return;
 
-            current = new ExecutionContext(current);
-            current.m_localValues[local] = newValue;
+            //
+            // Allocate a new Dictionary containing a copy of the old values, plus the new value.  We have to do this manually to 
+            // minimize allocations of IEnumerators, etc.
+            //
+            Dictionary<IAsyncLocal, object> newValues = new Dictionary<IAsyncLocal, object>(current.m_localValues.Count + (hadPreviousValue ? 0 : 1));
 
-            t_currentMaybeNull = current;
+            foreach (KeyValuePair<IAsyncLocal, object> pair in current.m_localValues)
+                newValues.Add(pair.Key, pair.Value);
 
+            newValues[local] = newValue;
+
+            //
+            // Either copy the change notification array, or create a new one, depending on whether we need to add a new item.
+            //
+            IAsyncLocal[] newChangeNotifications = current.m_localChangeNotifications;
             if (needChangeNotifications)
             {
                 if (hadPreviousValue)
-                    Contract.Assert(current.m_localChangeNotifications.Contains(local));
+                {
+                    Contract.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
+                }
                 else
-                    current.m_localChangeNotifications.Add(local);
+                {
+                    int newNotificationIndex = newChangeNotifications.Length;
+                    Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
+                    newChangeNotifications[newNotificationIndex] = local;
+                }
+            }
 
+            t_currentMaybeNull = new ExecutionContext(newValues, newChangeNotifications);
+
+            if (needChangeNotifications)
+            {
                 local.OnValueChanged(previousValue, newValue, false);
             }
         }