Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guid perf enhancements #20483

Closed
JoshLove-msft opened this issue Apr 19, 2021 · 8 comments
Closed

Guid perf enhancements #20483

JoshLove-msft opened this issue Apr 19, 2021 · 8 comments
Labels
Client This issue points to a problem in the data-plane of the library. Service Bus

Comments

@JoshLove-msft
Copy link
Member

JoshLove-msft commented Apr 19, 2021

Along the lines of the optimization that @danielmarbach made in #20427, we can optimize the lock renewal path in a similar way by renting an array for the Guid array.

Also, there are several locations where we convert the LockToken property on a Received message into a Guid. This can be avoided by just referencing the internal LockTokenGuid property.

Interestingly, there is a separate LockToken(singular) constant. It's possible that this could be used instead without needing an array at all, but we would need to test and confirm with the service team. @danielmarbach tested this out and the service doesn't accept it.

@JoshLove-msft JoshLove-msft added Service Bus Client This issue points to a problem in the data-plane of the library. labels Apr 19, 2021
@danielmarbach
Copy link
Contributor

Sent in #20488

I guess when all the existing tests pass this is proof enough that it works or not?

@danielmarbach
Copy link
Contributor

The tests failed with the service rejecting the request

What exactly do you want to return and return on this code path?

@JoshLove-msft
Copy link
Member Author

In that case, can we at least avoid allocating memory for each call by using the rent buffer approach? This is a hot path when using the processor.

@danielmarbach
Copy link
Contributor

Which memory? We get a string that is converted into a Guid. Do you mean the Guid Array itself?

Side note: Why is the lock token a string on the public API and not a guid?

@JoshLove-msft
Copy link
Member Author

JoshLove-msft commented Apr 19, 2021

Yes, I meant the Guid Array. When I profiled a test app I noticed this:
profile

As far as why the lock token is a string, this was likely just to make it easier to serialize and even bind to in Functions extensions. With the new SDK, the lock token is not as important as users cannot use it to settle a message.

I do think we can also eliminate the need to convert the Guid back to a string by just using the internal LockTokenGuid property internally. So I'd like to revise this issue to include that as well 😄

@JoshLove-msft JoshLove-msft changed the title Rent buffer for lock renewal rather than allocating an array each time Guid perf enhancements Apr 19, 2021
@danielmarbach
Copy link
Contributor

Pool rental for small arrays can be slower than allocation an array. I was planning to send in a PR to do OneOfList and OnOfArray which could be more beneficial than renting so small arrays. But already switching to internal Locktoken to avoid converting string to guid and vice versa would be good

@danielmarbach
Copy link
Contributor

If there is no time pressure on this I'm happy to give it a try

@danielmarbach
Copy link
Contributor

I very skeptic if it makes sense to rent and return the single array there honestly. I don't think it is worth the effort.

I also looked at the receive path and tried to optimize a bit for the single message receive by creating a "frugal list"

    internal struct CompactList<T> : IReadOnlyList<T>
        where T : class
    {
        private object _values;
        private int _capacity;

        public CompactList(int capacity)
        {
            _capacity = capacity;
            _values = default;
        }

        public Enumerator GetEnumerator()
        {
            return new Enumerator(_values);
        }

        /// <inheritdoc cref="GetEnumerator()" />
        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <inheritdoc cref="GetEnumerator()" />
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public int Count
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get
            {
                // Take local copy of _values so type checks remain valid even if the TValue is overwritten in memory
                object value = _values;
                if (value is T)
                {
                    return 1;
                }
                if (value is null)
                {
                    return 0;
                }
                // Not T, not null, can only be List<T>
                return Unsafe.As<List<T>>(value).Count;
            }
        }

        public T this[int index]
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get
            {
                // Take local copy of _values so type checks remain valid even if the TValue is overwritten in memory
                object value = _values;
                if (value is T t)
                {
                    if (index == 0)
                    {
                        return t;
                    }
                }
                else if (value != null)
                {
                    // Not string, not null, can only be string[]
                    return Unsafe.As<List<T>>(value)[index]; // may throw
                }

                return OutOfBounds(); // throws
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static T OutOfBounds()
        {
            return Array.Empty<T>()[0]; // throws
        }

        public void Add(T item)
        {
            if (_capacity == 1)
            {
                _values = item;
            }
            else
            {
                _values ??= new List<T>(_capacity);
                Unsafe.As<List<T>>(_values).Add(item);
            }
        }

        public struct Enumerator : IEnumerator<T>
        {
            private readonly List<T> _values;
            private T _current;
            private int _index;

            internal Enumerator(object value)
            {
                if (value is T t)
                {
                    _values = null;
                    _current = t;
                }
                else
                {
                    _current = default;
                    _values = Unsafe.As<List<T>>(value);
                }
                _index = 0;
            }

            public Enumerator(ref CompactList<T> values) : this(values._values)
            { }

            public bool MoveNext()
            {
                int index = _index;
                if (index < 0)
                {
                    return false;
                }

                List<T> values = _values;
                if (values != null)
                {
                    if ((uint)index < (uint)values.Count)
                    {
                        _index = index + 1;
                        _current = values[index];
                        return true;
                    }

                    _index = -1;
                    return false;
                }

                _index = -1; // sentinel value
                return _current != null;
            }

            public T Current => _current;

            object IEnumerator.Current => _current;

            void IEnumerator.Reset()
            {
                throw new NotSupportedException();
            }

            public void Dispose()
            {
            }
        }
    }

but then I realized since we are always expecting IReadonlyList the struct will get boxed anyway and then the specialized list is not worth anything really :(

But I sent this one to compensate :)

#20550

@github-actions github-actions bot locked and limited conversation to collaborators Mar 27, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Client This issue points to a problem in the data-plane of the library. Service Bus
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants