- 
                Notifications
    
You must be signed in to change notification settings  - Fork 5.2k
 
Description
Description
So, me and my bf were playing around with a simple exercise. Reverse a String.
Well we overcomplicated it, and found out that the fastest 2 ways are rather Interesting.
This is a test comprised of a few different ways that we ended up going about the problem.
Note: StringBuilder and Direct String Manipulation were removed, as with anything over 10_000 chars would take far too long.
Configuration
Windows 10 21H1 (OS Build 19043.1348) X64
Running an AMD Ryzen 5 3600 @ 4Ghz all core, stock cooler
Using BenchmarkDotNet 0.13.1
16 GB of ram
Regression?
There hasn't been any regression, just that with new performance optimizations it has exposed an area which can be optimized further.
Data
// * Summary *
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1348 (21H1/May2021Update)
AMD Ryzen 5 3600, 1 CPU, 12 logical and 6 physical cores
.NET SDK=6.0.100
  [Host]   : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
  .NET 6.0 : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
Job=.NET 6.0  Runtime=.NET 6.0
|                                          Method |      Mean |    Error |   StdDev |
|------------------------------------------------ |----------:|---------:|---------:|
|                        ReverseString_StackAlloc |  61.86 us | 0.463 us | 0.410 us |
|              ReverseString_AllocateArray_Pinned | 112.38 us | 1.998 us | 1.869 us |
| ReverseString_AllocateUninitializedArray_Pinned | 106.41 us | 0.608 us | 0.539 us |
|                     ReverseString_Uninitialized |  63.27 us | 0.276 us | 0.231 us |
|                       ReverseString_ToCharArray |  71.33 us | 0.764 us | 0.714 us |
|                    ReverseString_EmptyCharArray |  62.49 us | 0.391 us | 0.365 us |
|                      ReverseString_ArrayReverse |  71.85 us | 0.741 us | 0.657 us |
|                     ReverseString_AllocateArray |  66.04 us | 0.220 us | 0.206 us |
|                    ReverseString_FixedUnsafeFor |  65.89 us | 0.259 us | 0.242 us |
// * Hints *
Outliers
  FastStringTests.ReverseString_StackAlloc: .NET 6.0                        -> 1 outlier  was  removed (63.51 us)
  FastStringTests.ReverseString_AllocateUninitializedArray_Pinned: .NET 6.0 -> 1 outlier  was  removed (108.54 us)
  FastStringTests.ReverseString_Uninitialized: .NET 6.0                     -> 2 outliers were removed (64.02 us, 65.20 us)
  FastStringTests.ReverseString_ArrayReverse: .NET 6.0                      -> 1 outlier  was  removed (73.29 us)
// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 us   : 1 Microsecond (0.000001 sec)
Analysis
From the Benchmark results you can see that there have been across the board improvements. Nothing has gotten slower, however one thing to note is that Array.Reverse is slower then a Fixed Unsafe loop, which means that the underlying implementation of Array.Reverse could be improved.
Benchmark Code (1 File)
using System;
using System.Collections.Generic;
using System.Globalization;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Validators;
namespace Benchmarks
{
    [SimpleJob(runtimeMoniker: RuntimeMoniker.Net60)]
    public class FastStringTests
    {
        [ArgumentsSource(nameof(GetRandomString))]
        [Benchmark]
        public unsafe string ReverseString_StackAlloc(String Str)
        {
            Char* output_bptr = stackalloc Char[Str.Length];
            fixed (char* strPtr = Str)
            {
                Char* str_ptr = strPtr + Str.Length - 1;
                Char* output_ptr = output_bptr;
                while (str_ptr >= strPtr)
                    *(output_ptr++) = *(str_ptr--);
            }
            return new string(output_bptr, 0, Str.Length);
        }
        [ArgumentsSource(nameof(GetRandomString))]
        [Benchmark]
        public unsafe string ReverseString_AllocateArray(String Str)
        {
            Char[] output = GC.AllocateArray<Char>(Str.Length, false);
            fixed (char* output_bptr = output)
            fixed (char* strPtr = Str)
            {
                Char* str_ptr = strPtr + Str.Length - 1;
                Char* output_ptr = output_bptr;
                while (str_ptr >= strPtr)
                    *(output_ptr++) = *(str_ptr--);
            }
            return new string(output);
        }
        [ArgumentsSource(nameof(GetRandomString))]
        [Benchmark]
        public unsafe string ReverseString_AllocateArray_Pinned(String Str)
        {
            Char[] output = GC.AllocateArray<Char>(Str.Length, true);
            fixed (char* output_bptr = output)
            fixed (char* strPtr = Str)
            {
                Char* str_ptr = strPtr + Str.Length - 1;
                Char* output_ptr = output_bptr;
                while (str_ptr >= strPtr)
                    *(output_ptr++) = *(str_ptr--);
            }
            return new string(output);
        }
        [ArgumentsSource(nameof(GetRandomString))]
        [Benchmark]
        public unsafe string ReverseString_AllocateUninitializedArray_Pinned(String Str)
        {
            Char[] output= GC.AllocateUninitializedArray<Char>(Str.Length, true);
            fixed (char* output_bptr = output)
            fixed (char* strPtr = Str)
            {
                Char* str_ptr = strPtr + Str.Length - 1;
                Char* output_ptr = output_bptr;
                while (str_ptr >= strPtr)
                    *(output_ptr++) = *(str_ptr--);
            }
            return new string(output);
        }
        [ArgumentsSource(nameof(GetRandomString))]
        [Benchmark]
        public unsafe string ReverseString_Uninitialized(String Str)
        {
            Char[] output = GC.AllocateUninitializedArray<Char>(Str.Length, false);
            fixed (char* output_bptr = output)
            fixed (char* strPtr = Str)
            {
                Char* str_ptr = strPtr + Str.Length - 1;
                Char* output_ptr = output_bptr;
                while (str_ptr >= strPtr)
                    *(output_ptr++) = *(str_ptr--);
            }
            return new string(output);
        }
        [ArgumentsSource(nameof(GetRandomString))]
        [Benchmark]
        public unsafe string ReverseString_ToCharArray(String Str)
        {
            Char[] chars = Str.ToCharArray();
            fixed (char* charsPtr = chars)
            {
                char* output_bptr = charsPtr;
                fixed (char* strPtr = Str)
                {
                    Char* str_ptr = strPtr + Str.Length - 1;
                    Char* output_ptr = output_bptr;
                    while (str_ptr >= strPtr)
                        *(output_ptr++) = *(str_ptr--);
                }
                return new string(output_bptr, 0, Str.Length);
            }
        }
        [ArgumentsSource(nameof(GetRandomString))]
        [Benchmark]
        public unsafe string ReverseString_EmptyCharArray(String Str)
        {
            Char[] chars = new Char[Str.Length];
            fixed (char* charsPtr = chars)
            {
                char* output_bptr = charsPtr;
                fixed (char* strPtr = Str)
                {
                    Char* str_ptr = strPtr + Str.Length - 1;
                    Char* output_ptr = output_bptr;
                    while (str_ptr >= strPtr)
                        *(output_ptr++) = *(str_ptr--);
                }
                return new string(output_bptr, 0, Str.Length);
            }
        }
        [ArgumentsSource(nameof(GetRandomString))]
        [Benchmark]
        public unsafe string ReverseString_ArrayReverse(String Str)
        {
            Char[] chars = Str.ToCharArray();
            Array.Reverse(chars);
            return new string(chars);
        }
        [ArgumentsSource(nameof(GetRandomString))]
        [Benchmark]
        public unsafe string ReverseString_FixedUnsafeFor(String Str)
        {
            Char* output_bptr = stackalloc Char[Str.Length];
            fixed (char* strPtr = Str)
            {
                Char* str_ptr = strPtr + Str.Length - 1;
                Char* output_ptr = output_bptr;
                for(int x = 0; x < Str.Length; x++)
                    *(output_ptr++) = *(str_ptr--);
            }
            return new string(output_bptr, 0, Str.Length);
        }
        public IEnumerable<String> GetRandomString()
        {
            const int StringLength = 100_000; // must be divisable by 2
            Byte[] bytes = new Byte[StringLength / 2];
            (new Random()).NextBytes(bytes);
            yield return Convert.ToHexString(bytes);
        }
    }
    public static class Program
    {
        public static void Main()
        {
            BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).RunAllJoined(new Config());
        }
        public class Config : IConfig
        {
            public IOrderer Orderer => DefaultConfig.Instance.Orderer;
            public SummaryStyle SummaryStyle => DefaultConfig.Instance.SummaryStyle;
            public ConfigUnionRule UnionRule => DefaultConfig.Instance.UnionRule;
            public string ArtifactsPath => DefaultConfig.Instance.ArtifactsPath;
            public CultureInfo CultureInfo => DefaultConfig.Instance.CultureInfo;
            public ConfigOptions Options => DefaultConfig.Instance.Options;
            public IEnumerable<IAnalyser> GetAnalysers() => DefaultConfig.Instance.GetAnalysers();
            public IEnumerable<IColumnProvider> GetColumnProviders() => DefaultConfig.Instance.GetColumnProviders().Where((IColumnProvider provider) => provider != DefaultColumnProviders.Params);
            public IEnumerable<IDiagnoser> GetDiagnosers() => DefaultConfig.Instance.GetDiagnosers();
            public IEnumerable<IExporter> GetExporters() => DefaultConfig.Instance.GetExporters();
            public IEnumerable<IFilter> GetFilters() => DefaultConfig.Instance.GetFilters();
            public IEnumerable<HardwareCounter> GetHardwareCounters() => DefaultConfig.Instance.GetHardwareCounters();
            public IEnumerable<Job> GetJobs() => DefaultConfig.Instance.GetJobs();
            public IEnumerable<ILogger> GetLoggers() => DefaultConfig.Instance.GetLoggers();
            public IEnumerable<BenchmarkLogicalGroupRule> GetLogicalGroupRules() => DefaultConfig.Instance.GetLogicalGroupRules();
            public IEnumerable<IValidator> GetValidators() => DefaultConfig.Instance.GetValidators();
        }
    }
}