【问题标题】:Does "string" = "string" cost more memory"string" = "string" 是否消耗更多内存
【发布时间】:2018-01-23 17:11:02
【问题描述】:

作为我对这段代码进行重构的一部分,我遇到了这个 sn-p 代码,我正在讨论什么更正确/更有效?

之前

string CutHeadAndTail(string pattern)
{
    if (pattern[0] == '*')
    {
        pattern = pattern.Substring(1);
    }
    if (pattern[pattern.Length - 1] == '*')
    {
        pattern = pattern.Substring(0, pattern.Length - 1);
    }
    return pattern;
}

之后

private string RemoveAllowedAstrisks(string pattern)
{
    pattern = pattern[0] == '*'?pattern.Substring(1): pattern;
    pattern = pattern[pattern.Length - 1] == '*' ? pattern.Substring(0, pattern.Length - 1) : pattern;

    return pattern;
}

什么更好?
我正在考虑pattern = pattern[0] == '*'?pattern.Substring(1): pattern;这一行 意思是,从可读性的角度来看,我更喜欢第二种。但另一方面,这个表达式的含义是以下两个选项:

  1. pattern[0]=='*' --> 在这种情况下,模式将更改为 pattern.Substring(1)
  2. else --> pattern = pattern

如果我选择第一种方式(忽略命名等),我只有第一种选择:

  1. if (pattern[0] == '*')
    {
       pattern = pattern.Substring(1);
    }
    
  2. return pattern;
    

底线:pattern = pattern 行是否占用更多内存?

【问题讨论】:

  • 1.正如汉斯指出的那样,这并不重要。 2. string 是一个引用类型,所以 pattern = pattern 什么都不做(好吧,我不确定转换为 IL 和机器代码,如果没有得到很好的优化,引用可能会从内存复制到 cpu 寄存器和再次回到相同的内存位置)
  • 我不担心性能,但我想知道是否真的有区别?
  • @Roni 检查差异的最简单方法是查看 IL。使用 LinqPad 或类似工具
  • 最大的不同是原代码清晰易读。

标签: c# .net string memory


【解决方案1】:

如果您允许从字符串的开头和结尾修剪所有* 字符(如"**some string**"),那么您可以这样做:

private string RemoveAllowedAstrisks(string pattern)
{
    return pattern?.Trim('*');
}

【讨论】:

  • 不过,这稍微改变了原始方法的合同。 RemoveAllowedAstrisks("**foo**") 将导致原始版本中的"*foo*" 和此版本中的"foo"
  • 这应该是一个真正的错误
【解决方案2】:

我检查了生成的 IL 代码以查看创建的内容。

IL_000e: ldloc.0      // hello
IL_000f: ldc.i4.0     
IL_0010: callvirt     instance char [mscorlib]System.String::get_Chars(int32)
IL_0015: ldc.i4.s     42 // 0x2a
IL_0017: beq.s        IL_001c
IL_0019: ldloc.0      // hello
IL_001a: br.s         IL_0023
IL_001c: ldloc.0      // hello
IL_001d: ldc.i4.1     
IL_001e: callvirt     instance string [mscorlib]System.String::Substring(int32)
IL_0023: stloc.0      // hello

如您所见,ldloc.0 会在逻辑语句的: 分支运行时执行 (IL_0019)。紧接着代码跳转到IL_0023 执行stloc.0

这可能是值类型的问题,但string 是引用类型,这样的分配根本不会影响内存。为了证明这一点,我创建了一个大字符串并运行了一些 value = value 分配。堆大小从未改变。

string hello = new string('a', 3202340);
hello = hello;
hello = hello;

【讨论】:

    【解决方案3】:

    我更喜欢第一种方式,因为它更简单,并且使编译器更容易进行任何优化。

    说实话,我不知道编译器是否会在第二种方法中优化掉pattern = pattern。它可能。只有速度测试才能帮助您确定这一点。

    就个人而言,我发现第二种方法更简洁,但可读性不强。如果不需要的话,我不会编写将变量分配给自身的代码。

    更易读的方法是使用string.Trim(),但我认为效率较低。

    【讨论】:

    • 完全同意,但 Trim() 选项无法解决,因为它破坏了之前的行为。
    • @Roni:是的,这不是完全相同的逻辑。例如,如果字符串以多个星号 (*) 开头,它将删除所有这些星号,而您的代码只删除了第一个。
    【解决方案4】:

    可能最好的方法是via a regex

    using System.Text.RegularExpressions;
    
    ...
    private static Regex leadingTrailingAsterisks = new Regex("(^\\*)|(\\*$)");
    private static string RemoveAllowedAsterisks(string pattern)
    {
        return leadingTrailingAsterisks.Replace(pattern, "");
    }
    

    【讨论】:

    • OP 似乎担心 memory 的使用,而不是速度。我猜Regex 实例将需要比当前解决方案更多的内存。
    • 使用正则表达式代替几个基本的字符串操作调用几乎总是大材小用。
    • 他在询问内存和性能,对于较小的短语和正在编译,这将是矫枉过正。
    • 如果您因为内存成本而关心分配字符串两次,那么正则表达式会进行一站式替换
    • 即使只创建一次,也比当前解决方案多一次。
    【解决方案5】:

    以下如何:计算是否从前面或后面或两者都修剪一个字符,如果是,则返回一个子字符串,否则返回字符串本身。

    string CutHeadAndTail(string pattern)
    {
        var s = pattern[0] == '*' ? 1 : 0;
        var e = pattern[pattern.Length - 1] == '*' ? 1 : 0;
        return (s > 0 || e > 0) ?
            pattern.Substring(s, pattern.Length - s - e) :
            pattern;
    }
    

    【讨论】: