0
\$\begingroup\$

I was curious whether boxing impacts performance in a long chain of IObservable<T> operators where T is a value type, and whether performance can be improved by changing T to Box<T> from CommunityToolkit.HighPerformance. I tested this in the form of some unit tests. I also included a test without the long chain of operators to establish how much of the runtime is due to those operators.

The results I got were so surprising that I question whether I'm testing what I think I'm testing. The test where T is a bare value type runs fastest. This is the opposite of what I expected. Using Box<T> actually hurts performance, with runtime similar to an ordinary reference type. I get similar results when I run all the tests in a batch or run each one cold.

Do my tests actually demonstrate that T being a value type is faster, at least for these types?

    [TestClass]
    public class PerformanceTests
    {
        private const int RepeatCount = 10000000;
        private const int DoCount = 100;

        [TestMethod]
        public void TestReferenceType()
        {
            Observable.Repeat(new object(), RepeatCount).EmptyDo(DoCount).Subscribe(o => { });
        }

        [TestMethod]
        public void TestValueTypeBaseline()
        {
            Observable.Repeat(0, RepeatCount).Subscribe(o => { });
        }

        [TestMethod]
        public void TestValueType()
        {
            Observable.Repeat(0, RepeatCount).EmptyDo(DoCount).Subscribe(o => { });
        }

        [TestMethod]
        public void TestBoxT()
        {
            Observable.Repeat(Box<int>.GetFrom(0), RepeatCount).EmptyDo(DoCount).Subscribe(o => { });
        }
    }
    internal static class RxExtensions
    {
        public static IObservable<T> EmptyDo<T>(this IObservable<T> source, int count)
        {
            for(int i = 0; i < count; ++i)
            {
                source = source.Do(o => { });
            }
            return source;
        }
    }

Results:

Test Time
TestValueTypeBaseline 1.7s
TestValueType 7.2s
TestReferenceType 8.8s
TestBoxT 8.8s
\$\endgroup\$
6
  • 2
    \$\begingroup\$ This question really belongs on StackOverflow. Having said that, why do you assume a simple struct type (i.e. int) which is created on the stack would be slower than a reference type on the heap? Also, this should really be benchmarked with BenchmarkDotNet with a memory diagnoser. \$\endgroup\$
    – Hayden
    Sep 25 at 6:40
  • \$\begingroup\$ TestValueTypeBaseline lacks EmptyDo(DoCount) like the others are using. Is that intentional? \$\endgroup\$
    – RobH
    Sep 25 at 7:57
  • \$\begingroup\$ @Hayden It probably wouldn't be accepted on SO because it's "working code." I expected a value type passed through a Rx pipeline to be repeatedly boxed and unboxed. I repeated the experiment with a more complex struct type, which runs in ~8s instead of ~7.2s, still consistently ~12% faster than a reference type with the same properties. This seems to contradict Microsoft's advice to avoid using structs as method parameters, and the entire reason for the existence of Box<T>. \$\endgroup\$ Sep 25 at 17:45
  • \$\begingroup\$ @RobH Yes, that's mentioned in the question. \$\endgroup\$ Sep 25 at 17:45
  • \$\begingroup\$ @KevinKrumwiede - ah yes, I see it now. Where have you seen the advice about not using structs as method parameters? Also, I'd expect very little boxing in a library like Rx utilising generic types all the way down - what makes you think it will be a significant amount? \$\endgroup\$
    – RobH
    Sep 26 at 6:38

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.