C# – Deferred execution of Dispose for IDisposable objects

coptimization

I'm working on an application that does lots of encryption and decryption in-application and this is probably the number-one bottleneck, so I've been spending some time making performance tweaks to it. A lot of this has involved simply caching things in memory (I realize there is something of a security tradeoff in doing that), but I noticed during profiling that Dispose() was a fair amount of the time doing decryption (I believe for .NET cryptography stuff it zeroes over everything so this makes sense). So I came up with this idea:

Have a "dispose pool." Instead of using blocks, create objects, use them, return the result, and add them to the dispose pool in the finally block. Internally, the dispose pool uses a queue and a timer and every time the timer fires it dequeues the objects and disposes them.

I tried implementing this and it seems to work and improve performance, but then again, profiling it locally is not a really realistic use case. Is this sound? Am I likely to run into runaway performance issues I'm not currently thinking about?

I suppose I should add that this is an ASP.NET MVC application so everything revolves around requests.

Best Answer

You might take a look at Microsoft's ScheduledDisposable. I've never used it, but it looks as though it will queue your objects for disposal on a separate thread.

But if a pool is what you're looking for, I think this will work:

public interface IDisposableWrapper<TDisposable> : IDisposable where TDisposable : class, IDisposable
{
    TDisposable Reference { get; }
}

public interface IDisposableWrapperFactory<TDisposable> where TDisposable : class, IDisposable
{
    IDisposableWrapper<TDisposable> Create();
}

public sealed class ReusableDisposableFactory<TDisposable> : IDisposableWrapperFactory<TDisposable>, IDisposable
    where TDisposable : class, IDisposable
{
    readonly object padlock = new object();
    Func<TDisposable> getReference;
    Stack<TDisposable> stack;
    int capacity;

    public ReusableDisposableFactory(Func<TDisposable> getReference, int capacity)
    {
        if (getReference == null)
            throw new ArgumentNullException("getReference");
        this.stack = new Stack<TDisposable>(capacity);
        this.capacity = capacity;
        this.getReference = getReference;
    }

    bool IsDisposed { get { return stack == null; } }

    void ThrowOnDisposed()
    {
        if (IsDisposed)
            throw new ObjectDisposedException(GetType().Name);
    }

    sealed class ReusableDisposableWrapper : IDisposableWrapper<TDisposable>
    {
        ReusableDisposableFactory<TDisposable> factory;
        TDisposable reference;

        internal ReusableDisposableWrapper(ReusableDisposableFactory<TDisposable> factory, TDisposable reference)
        {
            if (factory == null)
                throw new ArgumentNullException("factory");
            this.factory = factory;
            this.reference = reference;
        }

        public bool IsDisposed { get { return reference == null; } }

        #region IDisposableWrapper<TDisposable> Members

        public TDisposable Reference
        {
            get { return reference; }
            private set { reference = value; }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            // Dispose of unmanaged resources.
            Dispose(true);
            // Suppress finalization.  Since this class actually has no finalizer, this does nothing.
            GC.SuppressFinalize(this);
        }

        void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Free any other managed objects here.
                var reference = Interlocked.Exchange(ref this.reference, null);
                if (reference != null)
                    factory.DisposeReference(reference);
            }
            // Free any unmanaged objects here. 
        }

        #endregion

        public override string ToString()
        {
            var theReference = Reference;
            if (IsDisposed || theReference == null)
                return base.ToString() + ": Disposed";
            else
                return base.ToString() + ": " + theReference.ToString();
        }
    }

    #region IDisposableWrapperFactory<TDisposable> Members

    public IDisposableWrapper<TDisposable> Create()
    {
        lock (padlock)
        {
            ThrowOnDisposed();
            TDisposable reference;
            if (stack.Count > 0)
            {
                reference = stack.Pop();
            }
            else
            {
                reference = getReference();
            }
            return new ReusableDisposableWrapper(this, reference);
        }
    }

    void DisposeReference(TDisposable reference)
    {
        lock (padlock)
        {
            if (reference == null)
                return;
            ThrowOnDisposed();
            if (stack.Count < capacity)
            {
                stack.Push(reference);
            }
            else
            {
                reference.Dispose();
            }
        }
    }

    #endregion

    #region IDisposable Members

    public void Dispose()
    {
        // Dispose of unmanaged resources.
        Dispose(true);
        // Suppress finalization.  Since this class actually has no finalizer, this does nothing.
        GC.SuppressFinalize(this);
    }

    void Dispose(bool disposing)
    {
        if (disposing)
        {
            lock (padlock)
            {
                if (!IsDisposed)
                {
                    while (stack.Count > 0)
                    {
                        var reference = stack.Pop();
                        reference.Dispose();
                    }
                    stack = null;
                    getReference = null;
                }
            }
        }
        // Free any unmanaged objects here. 
    }

    #endregion

    public override string ToString()
    {
        string str = base.ToString();
        if (!Monitor.TryEnter(padlock))
        {
            // Don't block for ToString()
            str = str + ", locked.";
        }
        else
        {
            try
            {
                if (IsDisposed)
                    str = str + ", Disposed";
                else
                    str = string.Format("{0}: {1} {2} cached", str, stack.Count, typeof(TDisposable).Name);
            }
            finally
            {
                Monitor.Exit(padlock);
            }
        }
        return str;
    }
}

Note the factory itself is disposable. I certainly would be reluctant to use this for finalizable objects however.