编译器使用经典交换对我的字符串反向方法进行了哪些优化?

What optimization is the compiler doing for my string reverse method with a classic swapping?

问题背景

我阅读了 this 关于如何尽快反转字符串的问题。我发现答案之一是比较不同的方法。在其中一个中,他们只是 运行 一个循环交换位置 i 的元素和位置 string.Length-1-i 的元素,但他们使用已知的通过 XOR 的棘手交换。我想知道与通过时间变量使用经典交换的相同方法相比,通过 XOR 使用交换来反转字符串的速度有多快。令人惊讶的是,与 XOR 相比,我的性能提高了近 50%。

问题

编译器是否在幕后做了一些神奇的事情,为什么我得到这个结果?

修改后的代码与基准测试

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ContestLibrary
{
    public class Problem
    {
        delegate string StringDelegate(string s);

        static void Benchmark(string description, StringDelegate d, int times, string text)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int j = 0; j < times; j++)
            {
                d(text);
            }
            sw.Stop();
            Console.WriteLine("{0} Ticks {1} : called {2} times.", sw.ElapsedTicks, description, times);
        }

        public static string ReverseXor(string s)
        {
            char[] charArray = s.ToCharArray();
            int len = s.Length - 1;

            for (int i = 0; i < len; i++, len--)
            {
                charArray[i] ^= charArray[len];
                charArray[len] ^= charArray[i];
                charArray[i] ^= charArray[len];
            }

            return new string(charArray);
        }

        public static string ReverseClassic(string s)
        {
            char[] charArray = s.ToCharArray();
            int len = s.Length-1;

            for (int i = 0; i < len; i++, len--)
            {
                char temp = charArray[len];
                charArray[len] = charArray[i];
                charArray[i] = temp;
            }
            return new string(charArray);
         }

        public static string StringOfLength(int length)
        {
            Random random = new Random();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < length; i++)
            {
                sb.Append(Convert.ToChar(Convert.ToInt32(Math.Floor(26 * random.NextDouble() + 65))));
            }
            return sb.ToString();
        }

        static void Main(string[] args)
        {

            int[] lengths = new int[] {1,10,100,1000, 10000, 100000};

            foreach (int l in lengths)
            {
                int iterations = 10000;
                string text = StringOfLength(l);
                Benchmark(String.Format("Classic (Length: {0})", l), ReverseClassic, iterations, text);
                Benchmark(String.Format("Xor (Length: {0})", l), ReverseXor, iterations, text);
                Console.WriteLine();    
            }
            Console.Read();
        }
    }
}

您可以使用 Array.Reverse 测试下面的代码,它比您提到的其他方法更有效,Array.Reverse 是原生编码的 ,它是维护和理解非常简单。

    public static string ReverseArray(string text)
    {
        if (text == null) return null;

        char[] array = text.ToCharArray();
        Array.Reverse(array);
        return new String(array);
    }

下面是完整的演示代码,

委托字符串 StringDelegate(string s);

    static void Benchmark(string description, StringDelegate d, int times, string text)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int j = 0; j < times; j++)
        {
            d(text);
        }
        sw.Stop();
        Console.WriteLine("{0} Ticks {1} : called {2} times.", sw.ElapsedTicks, description, times);
    }

    public static string ReverseArray(string text)
    {
        if (text == null) return null;

        // this was posted by petebob as well 
        char[] array = text.ToCharArray();
        Array.Reverse(array);
        return new String(array);
    }

    public static string ReverseXor(string s)
    {
        char[] charArray = s.ToCharArray();
        int len = s.Length - 1;

        for (int i = 0; i < len; i++, len--)
        {
            charArray[i] ^= charArray[len];
            charArray[len] ^= charArray[i];
            charArray[i] ^= charArray[len];
        }

        return new string(charArray);
    }

    public static string ReverseClassic(string s)
    {
        char[] charArray = s.ToCharArray();
        int len = s.Length-1;

        for (int i = 0; i < len; i++, len--)
        {
            char temp = charArray[len];
            charArray[len] = charArray[i];
            charArray[i] = temp;
        }
        return new string(charArray);
    }

    public static string StringOfLength(int length)
    {
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++)
        {
            sb.Append(Convert.ToChar(Convert.ToInt32(Math.Floor(26 * random.NextDouble() + 65))));
        }
        return sb.ToString();
    }

    static void Main(string[] args)
    {

        int[] lengths = new int[] { 1, 10, 100, 1000, 10000, 100000 };

        foreach (int l in lengths)
        {
            int iterations = 10000;
            string text = StringOfLength(l);                
            Benchmark(String.Format("Classic (Length: {0})", l), ReverseClassic, iterations, text);
            Benchmark(String.Format("Array (Length: {0})", l), ReverseArray, iterations, text);
            Benchmark(String.Format("Xor (Length: {0})", l), ReverseXor, iterations, text);
            Console.WriteLine();
        }
        Console.Read();
    }

注意:我已经更正了您反转字符串的代码,它没有像您的 post

那样正确地反转字符串

我同意哈罗德的观点。 XOR 交换并不比使用临时变量快。

我认为异或交换的神话可以追溯到为新变量分配内存是一项耗时的工作的时代。

最初我认为这可能与类型有关,也许在 int 数组中使用 XOR 交换会比在 char 数组中得到更好的结果,所以我已经在你的基准上建立了一个 int 数组只是文本 - 结果相同的结果(或多或少) int XOR 交换只是比使用临时变量慢。

更新:

正如 Damien_The_Unbeliever 在他对您的问题的评论中所写,R. Martinho Fernandes 在您链接到的问题中给出的 answer 实际上是第一页中唯一正确的答案反转字符串,即使是英语以外的语言。
事实上,基于那个答案和 one of it's comments 我已经编写了一个扩展方法来正确地反转字符串。

在我的母语(希伯来语)中,我们有各种点和符号来指定元音,数组的简单反转(或 IEnumerable)做错了,就像在法语示例中一样.

它比基于交换的常规实现慢得多(并且比基于 XOR 的交换慢大约 10 倍),但我几乎不认为这会成为问题,因为反转字符串不是你经常做的事情,并且在一个紧密的循环中反转许多字符串甚至更少。
根据你的benchmark方法测试,用这个方法反转10000个长度为10000的字符串大约需要13.5秒。
如果有人会写一个程序,实际上需要对这么长的字符串进行如此多的反转,我会感到非常惊讶。

所以这是我的国际安全字符串反向实现,我希望有人能从中受益:

public static string Reverse(this string source)
{
    if (string.IsNullOrEmpty(source))
    {
        return source;
    }
    var info = new StringInfo(source);
    var sb = new StringBuilder();

    for (int i = info.LengthInTextElements - 1; i > -1; i--)
    {
        sb.Append(info.SubstringByTextElements(i, 1));
    }
    return sb.ToString();
}

原因很简单,让我们看看每个函数在IL代码中有多少操作。 但首先,让我们看看这两个函数在时间上的真正差异。你说 XOR 函数比另一个慢了近 50%,当我 运行 Debug 模式下的代码时我得到了那个结果,但是你必须 运行 Release 模式下的代码才能完全允许优化器做它的工作:)。在释放模式下,XOR 函数几乎慢了 3 倍。

图片有for循环内部分的IL代码,这是唯一变化的部分。

第一张图是带临时变量的函数

第二张图是异或函数

如您所见,指令数量的差异很大,14 对 34。3 倍的时间差异来自一些操作,例如 conv.u2昂贵的。