29

If you profile a simple client application that uses SocketAsyncEventArgs, you will notice Thread and ExecutionContext allocations.

The source of the allocations is SocketAsyncEventArgs.StartOperationCommon that creates a copy of the ExecutionContext with ExecutionContext.CreateCopy().

ExecutionContext.SuppressFlow seems like a good way to suppress this allocation. However this method itself will generate allocations when ran in a new thread.

How can I avoid these allocations?

Romain Verdier
  • 12,833
  • 7
  • 57
  • 77
cao
  • 997
  • 9
  • 17
  • 2
    Why would you want to supress `ExecutionContext` flow? – Yuval Itzchakov Aug 12 '14 at 17:20
  • 2
    Suppressing the `ExecutionContext` flow is only a way to avoid an allocation (`SocketAsyncEventArgs` does not copy the context when the flow is suppressed). – cao Aug 12 '14 at 17:28
  • 3
    Why are you attempting to microbenchmark? Why is the allocation of an `ExecutionContext` so meaningful? – Yuval Itzchakov Aug 12 '14 at 17:55
  • 5
    I want to use `SocketAsyncEventArgs` in a very latency sensitive application where GC pauses are an issue. – cao Aug 13 '14 at 07:20
  • Are the allocations one per thread, or periodic? In other words, are you sure this is not simply the start-up cost, instead of a periodic allocation. I can understand the concern, since the whole point of SocketAsyncEventArgs is to avoid periodic allocations. If you could post a test program, I would be curious in seeing the allocations. – Frank Hileman Aug 19 '14 at 16:45
  • When `ExecutionContext.SuppressFlow` is used, the allocations only happen in new threads. However, the `SocketAsyncEventArgs` callback is periodically executed in newly created threads. This question is closely related to http://goo.gl/JoFqbP. – cao Aug 20 '14 at 17:13
  • 5
    Have you looked at using a non-GC language such as C++? It sounds like your application might not be a good fit for .NET. – theMayer Nov 23 '14 at 20:01
  • You should probably use your own pool of threads, and circulate among them, if you absolutely must. Other than, if that really seems like a bottleneck, which is `very very very unlikely`, (and I'd say, you're almost definitely doing something the wrong way), you should switch back to a non-GC language. –  Feb 26 '15 at 23:24
  • 1
    May be you can use BeginX end EndX methods instead and run the GC in server mode to avoid pause – agua from mars Feb 28 '15 at 21:03
  • What are you doing with your threads? What they have to handle? – user743414 Mar 12 '15 at 08:42
  • 2
    @FrankHileman Actually, the point of `SocketAsyncEventArgs` is to prevent fixing newly allocated buffers in memory - the problem you have when not using `SocketAsyncEventArgs` is memory fragmentation, not GC latency. If you just fix a buffer you created just for this one operation, you've got about 100% chance it will prevent heap compaction. And since the `Read` call can take pretty much forever, your heap(s) become crazy fragmented (our socket server reached over 99% fragmentation - gigabytes of memory free, but couldn't be reused). – Luaan May 20 '15 at 11:52

2 Answers2

2
  1. SocketAsyncEventArgs

    public class SocketAsyncEventArgs : EventArgs, IDisposable {
    //...
    // Method called to prepare for a native async socket call.
    // This method performs the tasks common to all socket operations.
    internal void StartOperationCommon(Socket socket) {
    
        //...
    
        // Prepare execution context for callback.
    
        if (ExecutionContext.IsFlowSuppressed()) {    
        // This condition is what you need to pass.
    
            // Fast path for when flow is suppressed.
    
            m_Context = null;
            m_ContextCopy = null;
    
        } else {
    
            // Flow is not suppressed.
    
            //...
    
            // If there is an execution context we need
             //a fresh copy for each completion.
    
            if(m_Context != null) {
                m_ContextCopy = m_Context.CreateCopy();
            }
        }
    
        // Remember current socket.
        m_CurrentSocket = socket;
       }
    
    
    
    
        [Pure]
        public static bool IsFlowSuppressed()
        {
            return  Thread.CurrentThread.GetExecutionContextReader().IsFlowSuppressed;
        }
       //...
        }
    
  2. ExecutionContext

    [Serializable] 
    public sealed class ExecutionContext : IDisposable, ISerializable
    {
    //...
    // Misc state variables.
    private ExecutionContext m_Context;
    private ExecutionContext m_ContextCopy;
    private ContextCallback m_ExecutionCallback;
    //...
    
    internal struct Reader
    {
        ExecutionContext m_ec;
        //...
         public bool IsFlowSuppressed 
        {
         #if !FEATURE_CORECLR
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
         #endif
            get { return IsNull ? false : m_ec.isFlowSuppressed; } 
        }
    
      } //end of Reader
    
    
    internal bool isFlowSuppressed 
       { 
        get 
        { 
            return (_flags & Flags.IsFlowSuppressed) != Flags.None; 
        }
        set
        {
            Contract.Assert(!IsPreAllocatedDefault);
            if (value)
                _flags |= Flags.IsFlowSuppressed;
            else
                _flags &= ~Flags.IsFlowSuppressed;
        }
       }
    
    
    [System.Security.SecurityCritical]  // auto-generated_required
    public static AsyncFlowControl SuppressFlow()
    {
        if (IsFlowSuppressed())
        {
            throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_CannotSupressFlowMultipleTimes"));
        }
        Contract.EndContractBlock();
        AsyncFlowControl afc = new AsyncFlowControl();
        afc.Setup();
        return afc;
    }
    //...
    }//end of ExecutionContext.
    
  3. AsyncFlowControl

    public struct AsyncFlowControl: IDisposable
    {
    private bool useEC;
    private ExecutionContext _ec;
    
    //... 
    
    [SecurityCritical]
    internal void Setup()
    {
        useEC = true;
        Thread currentThread = Thread.CurrentThread;
        _ec = currentThread.GetMutableExecutionContext();
        _ec.isFlowSuppressed = true;
        _thread = currentThread;
    }
    }
    
  4. Thread

    // deliberately not [serializable]
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(_Thread))]
    [System.Runtime.InteropServices.ComVisible(true)]
    public sealed class Thread : CriticalFinalizerObject, _Thread
    {
    
     //...
    
     [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        internal ExecutionContext.Reader GetExecutionContextReader()
        {
            return new ExecutionContext.Reader(m_ExecutionContext);
        }
    }
    

The only way to set isFlowSuppressed to true, to pass the condition in the StartOperationCommon method, is by calling Setup method, and the only call to Setup is in SuppressFlow method, wich you have discussed.

As you can see, SuppressFlow is the only solution.

TiyebM
  • 2,684
  • 3
  • 40
  • 66
  • 1
    You might want to add what `ExecutionContext` actually is, and what purpose it has. It's not there just for fun. – Luaan May 20 '15 at 11:55
0

Actually, SuppressFlow doesn't allocate. It returns a AsyncFlowControl, which is a struct. The proper solution basically is to call SendAsync and ReceiveAsync as follows:

public static bool SendAsyncSuppressFlow(this Socket self, SocketAsyncEventArgs e)
{
    var control = ExecutionContext.SuppressFlow();
    try
    {
        return self.SendAsync(e);
    }
    finally
    {
        control.Undo();
    }
}

public static bool ReceiveAsyncSuppressFlow(this Socket self, SocketAsyncEventArgs e)
{
    var control = ExecutionContext.SuppressFlow();
    try
    {
        return self.ReceiveAsync(e);
    }
    finally
    {
        control.Undo();
    }
}

I created these extension methods to make this a bit simpler and more explicit.

Traces with dotMemory showed that memory allocations really do go down to zero.

Pieter van Ginkel
  • 29,160
  • 8
  • 71
  • 111