C++字符串内存管理

13
上周我用C#写了几行代码,将一个大文本文件(30万行)加载到字典中。写这段代码只花了十分钟,执行时间不到一秒钟。
现在我正在将这段代码转换为C++(因为我需要在旧的C++ COM对象中使用它)。到目前为止,我已经花了两天时间。:(虽然生产力差异令人震惊,但我需要一些关于性能方面的建议。
它需要七秒钟来加载,而且更糟糕的是:释放所有CStringWs所需的时间也正好是那么长。这是不可接受的,我必须找到一种提高性能的方法。
有没有可能分配这么多字符串而不会看到这种可怕的性能下降?
我目前的想法是将所有文本都放入一个大数组中,然后让我的哈希表指向该数组中每个字符串的开头,并放弃CStringW。
但在此之前,你们C++专家有什么建议吗?
编辑:我的答案在下面。我意识到这对我来说是最快的路线,也是朝着我认为是正确方向 - 更多托管代码的方向迈出的步伐。

1
你真的需要向我们展示你现在所使用的代码,你的描述太模糊了,无法提供有用的反馈。 - 17 of 26
1
你肯定需要提供更多关于你的数据结构的信息... - Judge Maygarden
1
在我熟悉 .Net 之前,从 C++ 转向 C# - 没有任何关于框架(在我的情况下是 VCL)相似或不同的想法 - 这将与另一种方式一样具有挑战性。没有必要责怪 C++ 来提问你的问题。 - overslacked
很多人没有放弃6的选项。 - EvilTeach
1
发布源代码会非常有帮助,否则就像是在黑暗中开枪射击。 - Edouard A.
显示剩余5条评论
10个回答

13

是的,你不知道你有多正确!:-) 但对我来说,还有一个棘手的问题是,我想一次性读取ASCII、UTF8和Unicode,所以我必须将文件数据转换为我存储的数据。 - Dan Byström
1
@danbystrom,ASCII 可以被视为 UTF-8 的子集。UTF-8 是一种 Unicode 编码格式。如果您的文件采用 UTF-8 格式,则可以使用 Unicode 中定义的任何字符,并且现有的 ASCII 文本无需重新编码。 - CMircea

12
您正在扮演 Raymond Chen 的角色。他也做了同样的事情,用非托管 C++ 编写了一本中文词典。Rico Mariani 也这样做了,用 C# 编写。Mariani 先生制作了一个版本。陈先生写了6个版本,试图与 Mariani 版本的性能匹配。他基本上重写了 C/C++ 运行时库的重要部分以达到目标。

之后,托管代码获得了更多的尊重。GC 分配器是无法超越的。查看 此博客 帖子获取链接。这个 博客帖子 也可能会引起您的兴趣,因为它很有启发性,可以看到 STL 值语义是问题的一部分。


好的,引用Mariani的话来说,“所以,是的,你绝对可以击败CLR。”但我同意CLR的性能非常令人印象深刻。 - anon

10

哎呀,把CStrings删掉吧...

试试使用分析器。你确定没有运行调试代码吗?

使用std::string代替。

编辑:

我刚做了一项简单的测试,比较构造函数和析构函数。

CStringW似乎需要2到3倍的时间来执行新建/删除操作。

迭代1000000次,对每种类型进行新建/删除操作,没有其他任何操作 - 并在每个循环前后调用GetTickCount()函数。CStringW的时间始终是两倍长。

但这并不能解决您的整个问题,我怀疑还有其他问题导致了您的困扰。

编辑:我也不认为使用string或CStringW是真正的问题 - 还有其他原因导致了您的问题。

(但求上帝保佑,无论如何都要使用STL!)

你需要进行分析。这是一场灾难。


我会尝试使用std::string,敬请期待! :-) - Dan Byström
1
CString很糟糕。首先,正如我刚刚确认的那样,它比std::string慢。它不可移植。它是一个MFC hack - 连同所有其他MFC垃圾一起。别误会,我用了很多年,但我们使用的MFC越少,就越好... - Tim

4
如果是只读字典,那么以下内容应该适用于您。
Use fseek/ftell functionality, to find the size of the text file.

Allocate a chunk of memory of that size + 1 to hold it.

fread the entire text file, into your memory chunk.

Iterate though the chunk.

    push_back into a vector<const char *> the starting address of each line.

    search for the line terminator using strchr.

    when you find it, deposit a NUL, which turns it into a string.
    the next character is the start of the next line

until you do not find a line terminator.

Insert a final NUL character.

现在,您可以使用向量来获取指针,以便访问相应的值。

当您完成字典操作后,请释放内存,并在超出范围时使向量停止运行。

[编辑] 在dos平台上可能会更加复杂,因为行终止符是CRLF。

在这种情况下,使用strstr来查找它,并增加2以找到下一行的开头。


这个会很好用而且速度快。虽然我觉得没必要,因为他在代码中做了一些不好的事情,可能可以修复并使代码更易读和简单。 - Tim
导致程序性能下降的原因是构造/析构成本和内存分配/释放。 - EvilTeach
或者(如果Win32支持相应的功能)使用mmap将文件映射为MAP_PRIVATE(写时复制),这样可以避免复制文件。 - Pete Kirkham
当然是一个选择。我不知道额外的开销是多少。 - EvilTeach
如果使用C++构造会让你感觉更好,那就用吧。 - EvilTeach
显示剩余4条评论

3
问题不在于CString,而是你正在分配大量小对象——默认的内存分配器并没有针对这种情况进行优化。
编写自己的内存分配器——分配一大块内存,然后在分配时只需在其中移动指针。这其实就是.NET分配器所做的事情。当你准备好时,删除整个缓冲区即可。
我认为在(更多) Effective C++中有编写自定义new/delete运算符的示例。

虽然我从未尝试使用过,但是标准C++库的容器可以接受分配器作为参数,以便调整/优化项目创建。如果需要的话。 - Stephane Rolland

3
你正在使用什么样的容器来存储字符串?如果是一个由CStringW 组成的 std::vector,并且你没有预先使用reserve函数分配足够的内存,那么你肯定会受到影响。当vector达到其限制(不是很高)时,它通常会调整大小,然后将全部内容复制到新的内存位置,这可能会给你带来很大的影响。随着你的vector呈指数增长(即如果初始大小为1,则下一次分配2,4等,影响变得越来越少)。

还有一个要注意的问题是每个字符串的长度。(有时候:)


固定数组...你是一开始就分配了所有的内存,还是每次添加新字符串时调整数组大小? - Judge Maygarden
不,我从未重新分配它。这是一个哈希表,在罕见的情况下,如果我得到重复的哈希键,则会执行一些额外的操作,但在我的基准测试期间,我实际上会丢弃重复项,以确保没有性能问题。 - Dan Byström
看起来你已经完美地设置好了一切。你试过使用std::wstring吗?此外,现在是时候使用性能分析器来帮助你了。 - dirkgently

3
感谢大家的有见地的评论,给你们点赞!:-)
我必须承认,我完全没有准备好 - C#会以这种方式击败老旧的C++。请不要把这看作是对C++的冒犯,而是.NET Framework内置了一个非常出色的内存管理器。
我决定退后一步,在Interop领域中进行战斗!也就是说,我将保留我的C#代码,并让我的旧C++代码通过COM接口与C#代码交互。
有很多关于我的代码的问题,我会尝试回答其中的一些:
- 编译器是Visual Studio 2008,不,我没有运行调试版本。 - 文件是用UTF8文件阅读器读取的,我从一个微软员工的网站上下载的。它返回的是CStringW,大约30%的时间实际上都花在了那里读取文件。 - 我存储字符串的容器只是一个固定大小的指向CStringW的指针向量,它从未被重新调整大小。
编辑:我相信我得到的建议确实可以奏效,如果我投入足够的时间,我可能会打败C#代码。另一方面,这样做将不提供任何客户价值,坚持下去的唯一原因就是证明它可以做到...

2

将字符串加载到单个缓冲区中,解析文本以使用字符串终止符 ('\0') 替换换行符,并使用指向该缓冲区的指针将其添加到集合中。

或者 - 例如,如果您必须在加载期间执行 ANSI/UNICODE 转换 - 使用块分配器,牺牲删除单个元素。

class ChunkAlloc
{
   std::vector<BYTE> m_data;
   size_t m_fill;
   public:
     ChunkAlloc(size_t chunkSize) : m_data(size), m_fill(0) {}
     void * Alloc(size_t size)
     {
       if (m_data.size() - m_fill < size)
       {
          // normally, you'd reserve a new chunk here
          return 0;
       }
       void * result = &(m_data[m_fill]);
       m_fill += size;
       return m_fill;
     }
}
// all allocations from chuunk are freed when chung is destroyed.

我不会花10分钟将其破解,但30分钟的测试听起来还不错 :)


是的,是的,是的。这也是我在晚上想出来的! :-) - Dan Byström
参见 Raymond vs Rico (http://blogs.msdn.com/ricom/archive/2005/05/10/416151.aspx),这与你的项目非常相似,结果很明显:使用C++可以比C#应用程序更快地解决问题,但代价是巨大的。 - peterchen

1
在处理字符串类时,您应该始终查看不必要的操作,例如,不要经常使用构造函数、连接和类似的操作,特别是避免在循环中使用它们。我猜您使用CStringW是因为某些字符编码原因,所以您可能无法使用其他东西,这将是优化代码的另一种方式。

0

毫不奇怪,CLR的内存管理比MFC基于的一堆旧而肮脏的技巧要好得多:它至少比MFC年轻两倍,并且是基于池的。当我不得不使用字符串数组和WinAPI/MFC进行类似项目的工作时,我只是使用了用WinAPI的TCHAR实例化的std::basic_string和基于Loki::SmallObjAllocator的自己的分配器。在这种情况下,您还可以查看boost::pool(如果您希望它具有“std感觉”或必须使用早于7.1版本的VC++编译器的版本)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接