更有效的方法来确定字符串是否以一组令牌中的令牌开头?

时间:2009-01-27 19:14:44

标签: c# optimization

我现在正在做一些我现在正在处理的代码:

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#中是否有更有效的方法。这段代码需要每秒执行很多次,而且我对完成所有这些字符串比较所花费的时间不太满意。有什么建议? :)

11 个答案:

答案 0 :(得分:11)

一种优化方法是使用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 ran 1000000 times in 35.6033 ms
culture sensitive ran 1000000 times in 175.5859 ms

您还应该进行比较,以便首先比较最可能的代币(快乐路径)。

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

答案 1 :(得分:7)

与Vojislav的FSM答案概念类似,您可以尝试将常量放入trie。然后,您可以使用trie的单次遍历替换您的比较序列。

有一个描述here的C#trie实现。

答案 2 :(得分:3)

这是相当多的工作,但您可以根据每个令牌构建FSM。 FSM将逐个接受命令字符串中的字符;每个令牌都有一个最终状态,并且当命令不以任何令牌开始时,还有一个额外的最终状态。

答案 3 :(得分:3)

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

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
}

答案 4 :(得分:2)

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

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

编辑: 作为事后的想法,如果您知道哪些命令最常使用,您可以订购数组,以便这些命令在开始...如果您保留它们,也可以使用if语句执行此操作。

答案 5 :(得分:2)

编辑:鉴于对StopWatch的一个警告的误解,我的原始答案 与StringComparison.Ordinal一起表现不如StartsWith。即使使用所有正确的选项编译正则表达式,它也会慢一点,其性能可与使用StartsWith而不使用任何StringComparison设置相媲美。但是,正则表达式路径确实可以让您更灵活地匹配模式,而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,但只有当它存在于字符串的开头时...它还会拉出其他正确的CommandTypes。

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

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

答案 6 :(得分:1)

创建

IDictionary<string,CommandType> 

并用值填充它。

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

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

答案 7 :(得分:1)

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

但话又说回来,这真的很有用吗?我不会打赌它。

答案 8 :(得分:1)

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

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秒内完成数以千计的字符串比较应该没有汗水。

答案 9 :(得分:1)

特里肯定会成为最快的方法。如果前缀长度相同,您可以通过散列前缀来获得更快的实现,以获取数组的索引,但如果您不知道要散列多少字节,则会出现故障。

答案 10 :(得分:-3)

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

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

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