【问题标题】:More efficient way to determine if a string starts with a token from a set of tokens?确定字符串是否以一组标记中的标记开头的更有效方法?
【发布时间】:2009-01-28 22:22:22
【问题描述】:

我目前在我正在处理的一些代码中做这样的事情:

public CommandType GetCommandTypeFromCommandString(String command)
{
   if(command.StartsWith(CommandConstants.Acknowledge))
      return CommandType.Acknowledge;
   else if (command.StartsWith(CommandConstants.Status))
      return CommandType.Status;
   else if (command.StartsWith(CommandConstants.Echo))
      return CommandType.Echo;
   else if (command.StartsWith(CommandConstants.Warning))
     return CommandType.Warning;
     // and so on

   return CommandType.None;
}

我想知道在 C# 中是否有更有效的方法来执行此操作。这段代码需要每秒执行很多次,我对完成所有这些字符串比较所花费的时间不太满意。有什么建议么? :)

【问题讨论】:

  • CommandConstants.Acknowledge 等常量有多长(有多少个字符)?
  • 2 到 3 个字符,我认为最大是 4 个。
  • 您的测试是否在顶部使用最常见的命令类型进行排序?

标签: c# optimization


【解决方案1】:

一种优化是使用 StringComparison 枚举来指定您只需要序数比较。像这样:

if(command.StartsWith(CommandConstants.Acknowledge, StringComparison.Ordinal))
    return CommandType.Acknowledge;

如果您不指定字符串比较方法,则将使用当前文化进行比较,这会减慢速度。

我做了一些(真的很天真)基准测试:

var a = "foo bar foo";
var b = "foo";

int numTimes = 1000000;

Benchmark.Time(() => a.StartsWith(b, StringComparison.Ordinal), "ordinal", numTimes);
Benchmark.Time(() => a.StartsWith(b), "culture sensitive", numTimes);

产生了以下结果:

ordinal 在 35.6033 毫秒内运行了 1000000 次 文化敏感在 175.5859 毫秒内运行 1000000 次

您还应该对比较进行排序,以便首先比较最可能的标记(快乐路径)。

这些优化是使您当前的实现性能更好的简单方法,但如果性能真的很关键(我的意思是非常关键),您应该考虑实现某种状态机。

【讨论】:

  • 很棒的提示...您必须有一些出色的机器,以使其在 35 毫秒内运行...我认为我的机器很好,但它在 10,000 次中运行您的代码超过一百万次迭代小姐。相比之下,使用正则表达式大约需要 6000 毫秒。
  • 不,这里没有超级计算机...我提供了一些计时代码,您可以在snipplr.com/view/11585/… 上尝试您的计算机
  • @Markus:谢谢,我去看看。
  • @Markus:好的,它使用我用于计时的相同秒表......我想知道为什么我的计时与你的相差如此之大。
  • @Markus:好的,我从你那里学到了一些关于秒表的新东西 - sw.Elapsed.TotalMilliseconds 与 sw.ElapsedMilliseconds 不同......我从来没有注意到还有另一种方法可以获取经过的时间非常感谢!
【解决方案2】:

在概念上类似于 Vojislav 的 FSM 答案,您可以尝试将常量放入 trie。然后,您可以将比较序列替换为对 trie 的一次遍历。

here 描述了一个 C# trie 实现。

【讨论】:

  • 谢谢!这可能就是我最终要做的——我非常喜欢这种方法。我忘了尝试!
  • 谢谢,我忘了尝试!
【解决方案3】:

这是一项相当多的工作,但您可以基于每个令牌构造一个FSM。 FSM 会一一接受 command 字符串中的字符;每个令牌都有一个最终状态,当命令不以任何令牌开头时,它会有一个额外的最终状态。

【讨论】:

    【解决方案4】:

    我认为使用正则表达式和字典可以做得更好:

    static Regex reCommands = new Regex("^(cmd1|cmd2|cmd3|cmd4)", RegexOptions.Compiled);
    static Dictionary<string, CommandType> Commands = new Dictionary<string, CommandType>();
    private static InitDictionary()
    {
        Commands.Add("cmd1", cmdType1);
        Commands.Add("cmd2", cmdType2);
        Commands.Add("cmd3", cmdType3);
        Commands.Add("cmd4", cmdType4);
    }
    public CommandType GetCommandTypeFromCommandString(String command)
    {
        Match m = reCommands.Match(command);
        if (m.Success)
        {
            return Commands[m.Groups[1].Value];
        }
        return CommandType.None; // no command
    }
    

    【讨论】:

      【解决方案5】:

      我认为你应该追求更多的可读性而不是担心效率。这些操作非常快。我第二个 Serge,这部分代码不太可能是瓶颈。我会这样做:

      public CommandType GetCommandTypeFromCommandString(String command)
      {
         for(String currentCommand : allCommands) {
             if(command.StartsWith(currentCommand))
                 return currentCommand;
         }
         return CommandType.None;
      }
      

      编辑: 作为事后的想法,如果您知道哪些命令最常使用,您可以对数组进行排序,以便这些命令位于开头...如果您保留它们,也可以使用 if 语句来执行此操作。

      【讨论】:

        【解决方案6】:

        编辑:鉴于对 StopWatch 的警告之一的误解,我的原始答案表现得不如 StartsWith 与 StringComparison.Ordinal 结合使用。即使您使用所有正确的选项编译正则表达式,它也会稍微慢一些,其性能与使用没有任何 StringComparison 设置的 StartsWith 相当。但是,正则表达式路由 确实 为您提供了更多的灵活性来匹配模式,而 StartsWith 没有,所以我把我原来的答案留给后代......

        原答案:

        我不得不承认,我不确定您到底在寻找什么 - 但是,在我看来,这种代码通常会解析一些描述的日志文件,以提取有用的信息。为了节省您长时间进行所有字符串比较,您可以生成枚举中值的正则表达式,然后使用匹配项解析正确的枚举项:

        public enum CommandType
        {
            Acknowledge,
            Status,
            Echo,
            Warning
        }
        static public CommandType? GetCommandTypeFromString(String command)
        {
            var CommandTypes = String.Join("|", Enum.GetNames(typeof(CommandType)));
            var PassedConstant = Regex.Match(command, String.Format("(?i:^({0}))", CommandTypes)).Value;
            if (PassedConstant != String.Empty)
                return (CommandType)Enum.Parse(typeof(CommandType), PassedConstant, true);
            return null;
        }
        
        static void Main(string[] args)
        {
            Console.WriteLine(GetCommandTypeFromString("Acknowledge that I am great!").ToString());
        }
        

        这会从我的字符串的开头提取 CommandType.Acknowledge,但前提是它存在于字符串的开头......它也会提取其他正确的 CommandType。

        对接受的答案进行类似的基准测试,我的性能提高了大约 40%。我在 10421 毫秒内运行了超过一百万次迭代,但我的代码只用了 6459 毫秒。

        当然,虽然您使用的 if 语句可能看起来不像您希望的那样高效,但它仍然比使用正则表达式更容易阅读...

        【讨论】:

          【解决方案7】:

          创建一个

          IDictionary<string,CommandType> 
          

          并用值填充它。

          您无需比较所有内容...只需在表格中查找即可。

          您还需要更好地定义命令语法。例如,命令和行的其余部分之间需要一个空格...

          【讨论】:

          • 我不太明白这个答案如何帮助解决检测以命令令牌开头的字符串的问题。
          • 问题是我得到这样的命令字符串:ER099。我无法使用类似的东西进行查找。
          • 该命令的语法是什么?你如何定义一个错误的命令(除了它不以任何命令字符串开头)
          • 如果它不以命令字符串开头,这是一个错误的命令。我正在使用的协议似乎很快就被拼凑在一起,没有经过太多思考,所以没有可以依赖的好的语法。
          • 没有编写解析器,我真的不想这样做——在这种情况下,我会认为这有点矫枉过正。
          【解决方案8】:

          “很多很多”是多少次?我严重怀疑这部分代码是瓶颈。假设它们都是不同的,您可能可以使用基于每个命令的第一个字母的 switch 语句对其进行一点点优化。

          不过话说回来,它真的有用吗?我不会赌太多。

          【讨论】:

            【解决方案9】:

            我做了类似扩展方法的事情:

            public static bool StartsWith(this string s, params string[] candidate)
            {
                string match = candidate.FirstOrDefault(t => s.StartsWith(t));
                return match != default(string);
            }
            

            现在这只是一个谓词,它返回数组中的字符串是否以给定字符串开头,但您可以对其进行一些修改:

            public static int PrefixIndex(this string s, params string[] candidate)
            {
                int index = -1;
                string match = candidate.FirstOrDefault(t => { index++; return s.StartsWith(t); });
                return match == default(string) ? -1 : index;
            }
            

            在使用中它会是:

            int index = command.PrefixIndex(tokenStrings);
            
            if (index >= 0) {
                // convert to your enum
            }
            

            在评论中,我看到您想在 1/40 秒内进行 30 次字符串比较。我的朋友,你应该可以在 1 MHz 的机器上做到这一点。在 1/40 秒内进行数千次字符串比较应该不费吹灰之力。

            【讨论】:

              【解决方案10】:

              trie 几乎可以肯定是最快的方法。如果前缀长度相同,您可以通过对前缀进行散列以获取数组索引来提出更快的实现,但如果您不知道要散列多少字节,则该方法会失败。

              【讨论】:

                【解决方案11】:

                注意:我不是在演示使用异常处理来控制程序流。如果 Enum 的字符串名称不存在,Enum.Parse 将引发异常。 Catch 子句只返回 None 的默认 CommandType,就像提问者的示例代码一样。

                如果对象只是返回给定字符串名称的实际 Enum 对象,你不能使用:

                try
                {
                    return (CommandType)Enum.Parse(typeof(CommandType), strCmdName, true);
                }
                catch (Exception)
                {
                    return CommandType.None;
                }
                

                【讨论】:

                • 使用异常来控制程序流是一个非常糟糕的主意。
                • 我没有控制程序流程。如果给定的字符串值无法映射到现有的 Enum,Enum.Parse 将引发异常。 Catch 只返回 None 的默认命令 - 就像提问者的示例代码一样。我认为投反对票是不必要的。
                • 我忘了,Enum 没有 TryParse 方法。我收回它,撤销我的反对票,并道歉。 :)
                • 感谢“unforgiven3”。大多数人不会有礼貌地“承认并解决疏忽”。非常感谢。
                • 我不确定我是否会否决它,但这仍然不是一个好的答案:它没有解决主要问题,不是“如何将字符串转换为枚举? "但是“如何将字符串转换为枚举比我现在使用的方法更有效?”
                猜你喜欢
                • 2023-03-03
                • 1970-01-01
                • 2012-05-03
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多