内存方面,是将长非动态字符串存储为单个字符串对象更好,还是让程序从它的重复部分构建它

本文关键字:字符串 程序 构建 复部 单个 方面 动态 存储 对象 内存 更好 | 更新日期: 2023-09-27 18:30:16

这是一个有点奇怪的问题,更像是一个实验,我需要任何东西,但我仍然对答案感到好奇:如果我有一个我提前知道永远不会改变但(大部分)由重复部分组成的字符串,那么将字符串作为单个字符串对象会更好吗? 在需要时调用并完成它 - 或者我应该将字符串分解为表示重复部分的较小字符串并在需要时连接它们?

让我举个例子:假设我们有一个天真的程序员想要创建一个正则表达式来验证 IP 地址(换句话说,我知道这个正则表达式不会按预期工作,但它有助于显示我所说的重复部分的含义,并为示例的第二部分节省了一些键入时间)。 所以他写了这个函数:

 private bool isValidIP(string ip)
 {
   Regex checkIP = new Regex("''d''d?''d?''.''d''d?''d?''.''d''d?''d?''.''d''d?''d?");
   return checkIP.IsMatch(ip);
 }

现在我们的年轻程序员注意到他有"''d","''d?"和"''."只是重复了几次。 这给了他一个想法,他既可以节省一些存储空间,又可以帮助提醒自己这对以后意味着什么。 所以他重新制作了函数:

 private bool isValidIP(string ip)
 {
   string escape = "''";
   string digi = "d";
   string digit = escape + digi;
   string possibleDigit = digit + '?';
   string IpByte = digit + possibleDigit + possibleDigit;
   string period = escape + '.';
   Regex checkIP = new Regex(IpByte + period + IpByte + period + IpByte + period + IpByte);
   return checkIP.IsMatch(ip);
 }

第一种方法很简单。 它只是在程序的指令中存储 38 个字符,每次调用函数时都会将这些字符读入内存。第二种方法将(我怀疑)两个 1 长度的字符串和两个字符存储到程序的指令中,以及将这四个连接成不同顺序的所有调用。 当程序被调用时,这会在内存中创建至少 8 个字符串(六个命名字符串,正则表达式前四部分的临时字符串,然后是从前一个字符串创建的最后一个字符串 + 正则表达式的三个字符串)。 第二种方法也恰好有助于解释正则表达式正在寻找什么 - 尽管不是最终正则表达式的样子。 它还可以帮助重构,例如,如果我们假设的程序员意识到他当前的正则表达式将允许在 IP 地址中不仅仅是 0-255,并且可以更改构成部分而无需找到需要修复的每个项目。

同样,哪种方法会更好? 它会像在程序大小与内存使用之间进行权衡一样简单吗? 当然,对于这样简单的事情,权衡充其量是可以忽略不计的,但是更大、更复杂的字符串呢?

哦,是的,IP 地址的更好正则表达式是:

 ^(25[0-5]|2[0-4]''d|[01]?''d''d?)(''.(25[0-5]|2[0-4]''d|[01]?''d''d?)){3}$

不会像一个例子一样好,不是吗?

内存方面,是将长非动态字符串存储为单个字符串对象更好,还是让程序从它的重复部分构建它

第一个是迄今为止更好的选择。 原因如下

  1. 它更清楚了。

  2. 它更便宜。任何时候你声明一个新对象,这都是一个"昂贵"的过程。 你必须在堆上为它腾出空间(至少对于字符串来说)。 是的,理论上你可以节省一个字节左右,但是你花更多的时间(可能,我还没有测试过它)来遍历和为每个字符串分配空间,额外的内存指令等。 更不用说请记住,您还必须考虑GC的使用。 你不断分配字符串,最终你将不得不与它一起占用进程刻度。你真的想进行优化,我可以很容易地看出这段代码没有达到应有的效率。 一件事没有常量,这意味着您可能正在创建比您需要的更多的对象,而不是让编译器针对不需要更改的字符串进行优化。 这让我想到,作为一个审查这段代码的人,我需要仔细看看将要发生的事情,并找出是否有问题。

  3. 它更清楚(是的,我又说了一遍)。 你想做一个学术追求,看看你能有多高效。 很酷。 我明白了。 我自己做。 这很有趣。 我从不让它溜进生产代码中。 我不在乎丢失一个勾号,我关心的是生产中出现错误,我关心其他程序员是否可以理解我的代码的作用。 阅读别人的代码已经够难的了,我不想增加额外的任务,让他们必须尝试弄清楚我投入了哪些微优化,以及如果他们"轻推"错误的代码段会发生什么。

  4. 你击中了另一点。如果原始正则表达式错误怎么办。 谷歌会告诉你这个问题已经解决了。 你可以谷歌另一个正确的正则表达式,并且已经过测试。 你不能谷歌"我的代码有什么问题"。 你可以把它贴在SO上,但这意味着其他人必须参与进来并查看它。

以下是使第一个示例轻松赢得赛马的方法:

 Regex checkIP = new Regex(
   "''d''d?''d?''.''d''d?''d?''.''d''d?''d?''.''d''d?''d?");
 private bool isValidIP(string ip)
 {
   return checkIP.IsMatch(ip);
 }

声明一次,一遍又一遍地重复使用。 如果您花时间动态重新创建正则表达式以保存一些正则表达式,请不要这样做。从技术上讲,您可以这样做,但仍然只创建一次对象,但这比将其移动到类级变量要多得多。

您实际上是在尝试在此处与编译器博弈并实现自己的字符串压缩。 对于您描述的字符串文字类型,您的节省似乎只是从编译的二进制文件中削减了数十个字节,由于内存对齐,甚至可能无法实现。 为了换取这几个字节的节省空间,这种方法增加了代码复杂性和运行时开销,更不用说调试难度了。

存储很便宜。 为什么要让你的生活(以及你同事的生活)更难? 保持你的代码简单、清晰和明显 - 你稍后会感谢自己。

第二个在内存消耗方面更糟糕,因为每次连接两个字符串时,内存中都会有三个字符串。

尽管编译器通过为您创建一个StringBuilder开始处理字符串常量的一些实例,但我仍然会投票支持第一个内存密集度较低,因为如果系统确实为您创建了StringBuilder,您将为此付出开销,如果它看不到第一段......

我现在很好奇编译正则表达式将如何影响内存使用。

这里的储蓄是虚幻的,把这个字符串分开是一个很大的过头。节省微不足道的内存并使如此简单的代码复杂化是没有意义的。您不会看到任何节省,但下一个维护该代码的人将花费 10 倍以上的时间来理解它。

字符串

是不可变的,所以如果你的字符串从不/很少改变,请把它放在一个整体中。密集的字符串连接给垃圾回收器带来了额外的压力。

除非你的字符串和子字符串很大,并且你至少可以节省千字节,否则不要把时间和精力花在这样的优化上。