使用 STL 从文件中删除除最后 500,000 个字节之外的所有字节

Posted

技术标签:

【中文标题】使用 STL 从文件中删除除最后 500,000 个字节之外的所有字节【英文标题】:Remove all but the last 500,000 bytes from a file with the STL 【发布时间】:2008-12-06 00:19:49 【问题描述】:

我们的日志记录类在初始化时会将日志文件截断为 500,000 字节。从那时起,日志语句将附加到文件中。

我们这样做是为了降低磁盘使用率,我们是一种商品最终用户产品。

显然保留前 500,000 个字节没有用,所以我们保留最后 500,000 个字节。

我们的解决方案存在一些严重的性能问题。有什么有效的方法来做到这一点?

【问题讨论】:

嘿,你不是 Last.FM 的成员之一吗?我认得你的头像! 我也认识你,SeriousSpoon :) 【参考方案1】:

“我可能会创建一个新文件,在旧文件中查找,从旧文件到新文件进行缓冲读/写,将新文件重命名为旧文件。”

我认为你最好还是简单点:

#include <fstream>
std::ifstream ifs("logfile");  //One call to start it all. . .
ifs.seekg(-512000, std::ios_base::end);  // One call to find it. . .
char tmpBuffer[512000];
ifs.read(tmpBuffer, 512000);  //One call to read it all. . .
ifs.close();
std::ofstream ofs("logfile", ios::trunc);
ofs.write(tmpBuffer, 512000); //And to the FS bind it.

这可以避免文件重命名,只需将最后的 512K 复制到缓冲区,以截断模式打开日志文件(清除日志文件的内容),然后将相同的 512K 吐回到文件的开头。

请注意,上面的代码还没有经过测试,但我认为这个想法应该是合理的。

您可以将 512K 加载到内存中的缓冲区中,关闭输入流,然后打开输出流;这样,您将不需要两个文件,因为您输入、关闭、打开、输出 512 个字节,然后继续。您可以通过这种方式避免重命名/文件重定位魔法。

如果您在某种程度上不反对将 C 与 C++ 混合使用,您也可以:

(注意:伪代码;我不记得我脑海中的 mmap 调用)

int myfd = open("mylog", O_RDONLY); // Grab a file descriptor
(char *) myptr = mmap(mylog, myfd, filesize - 512000) // mmap the last 512K
std::string mystr(myptr, 512000) // pull 512K from our mmap'd buffer and load it directly into the std::string
munmap(mylog, 512000); //Unmap the file
close(myfd); // Close the file descriptor

取决于很多事情,mmap 可能比寻找更快。如果你好奇的话,谷歌搜索“fseek vs mmap”会产生一些有趣的信息。

HTH

【讨论】:

我对此没有异议,只是很难让它在失败时变得健壮。我的意思是,让我的版本也能抵抗故障并不是一件容易的事,而仅仅是因为它取决于平台和文件系统的特性。一旦你知道你可以做必要的工作。 哦,是的,而且由于我过去做过大量的嵌入式和移动编程,在堆栈上放置一个 500k 的缓冲区让我的牙齿蠕动 ;-) 我感觉堆栈上的 500K 缓冲区会破坏几个编译器(堆栈帧大小由编译器控制)。使用向量 在这种情况下,向量的唯一“开销”是大小、指针和容量可能会写入堆栈。我想如果您更喜欢异常非安全性,则向量可能被认为是不必要的开销,但您在大多数编译器上或多或少地获得了 RAII。 所以我会说完全相反:当向量可以使用时,不需要使用 new 创建数组。【参考方案2】:

我可能会:

创建一个新文件。 在旧文件中查找。 从旧文件到新文件进行缓冲读/写。 将新文件重命名为旧文件。

要做前三个步骤(省略了错误检查,例如,如果文件小于 500k 大,我不记得 seekg 做了什么):

#include <fstream>

std::ifstream ifs("logfile");
ifs.seekg(-500*1000, std::ios_base::end);
std::ofstream ofs("logfile.new");
ofs << ifs.rdbuf();

那么我认为你必须做一些非标准的事情来重命名文件。

很明显,您需要 500k 的可用磁盘空间才能使其正常工作,因此,如果您截断日志文件的原因是因为它刚刚填满了磁盘,那就不好了。

我不确定为什么搜索速度很慢,所以我可能会遗漏一些东西。我不希望寻找时间取决于文件的大小。可能取决于文件,我不确定这些函数是否在 32 位系统上处理 2GB+ 文件。

如果复制本身很慢,那么根据平台的不同,您可能可以通过使用更大的缓冲区来加速它,因为这减少了系统调用的次数,也许更重要的是减少了磁盘磁头必须寻找的次数在读点和写点之间。为此:

const int bufsize = 64*1024; // or whatever
std::vector<char> buf(bufsize);
...
ifs.rdbuf()->pubsetbuf(&buf[0], bufsize);

用不同的值测试它,看看。您也可以尝试增加 ofstream 的缓冲区,我不确定这是否会有所作为。

请注意,在“实时”日志文件上使用我的方法很麻烦。例如,如果在副本和重命名之间附加了一个日志条目,那么您将永远丢失它,并且您尝试替换的文件上的任何打开句柄都可能导致问题(它会在 Windows 和 linux 上失败)将替换文件,但旧文件仍会占用空间并仍会被写入,直到句柄关闭)。

如果截断是从执行所有日志记录的同一线程完成的,那么没有问题,您可以保持简单。否则,您将需要使用锁或其他方法。

这是否完全健壮取决于平台和文件系统:移动和替换可能是也可能不是原子操作,但通常不是,因此您可能必须重命名旧文件,然后重命名新文件,然后删除旧文件,并进行错误恢复,在启动时检测是否有重命名的旧文件,如果有,将其放回并重新启动截断。 STL 无法帮助您处理平台差异,但有 boost::filesystem。

抱歉,这里有很多警告,但很大程度上取决于平台。如果您在 PC 上,那么我很奇怪为什么复制区区 0.5 兆的数据需要任何时间。

【讨论】:

【参考方案3】:

如果您碰巧使用 Windows,请不要费心复制零件。只需调用 FSCTL_SET_SPARSEFSCTL_SET_ZERO_DATA 告诉 Windows 您不再需要第一个字节

【讨论】:

【参考方案4】:

如果您可以在重新初始化之间生成几 GB 的日志文件,那么似乎仅在初始化时截断文件并没有真正的帮助。

我认为我会尝试提出一种专门的文本文件格式,以便始终将内容替换到位,并带有指向“当前”行的指针。您将需要一个恒定的线宽来仅分配一次磁盘空间,并将指针放在该文件的第一行或最后一行。

这样,文件将永远不会增长或缩小,并且您将始终拥有最后 N 个条目。

N=6 的插图(“|”表示空格填充直到那里):

#myapp 日志文件,行 = 6,宽度 = 80,指针 = 4 |
[2008-12-01 15:23] foo烤蛋糕|
[2008-12-01 16:15] foo 烤好了蛋糕 |
[2008-12-01 16:16] foo 吃蛋糕 |
[2008-12-01 16:17] foo告诉bar:我给你做了蛋糕,但是我吃过了|
[2008-12-01 13:53] 酒吧想点蛋糕|
[2008-12-01 14:42] bar 告诉 foo:sudo 给我烤个蛋糕 |

【讨论】:

【参考方案5】:

另一种解决方案是让日志记录类检测日志文件大小何时超过 500k,然后打开一个新的日志文件,然后关闭旧的。

然后日志类会查看旧文件,并删除最旧的文件

记录器有两个配置参数。

    500k 为何时开始新日志的阈值 要保留的旧日志的数量。

这样,日志文件管理将是一个自我维护的事情。

【讨论】:

【参考方案6】:

所以你想要文件的结尾——你把它复制到某种缓冲区来做什么?你是什​​么意思'写回'到文件中。你的意思是它覆盖了文件,在初始化时将原始文件截断为 500k 字节+它添加了什么?

建议:

重新思考你在做什么。如果这行得通并且是所期望的,那么它有什么问题?为什么要改变?有性能问题吗?您是否开始想知道所有日志条目都去了哪里?对于此类问题,提供比发布现有行为更多的问题最有帮助。除非他们知道完整的问题,否则没有人可以对此发表完全评论——因为它是主观的。

如果是我和我的任务是重新设计你的日志记录机制,我会建立一个机制来将日志文件截断为:时间长度或大小。

【讨论】:

我们设法创建了一个多 GB 的日志文件。该程序从未成功启动,因为我们的截断代码太慢了。 GDB 提示它卡在 seekg() 中,在文件结束前寻找 500,000 字节。 是的,我想说的是,我们将文件的最后 500,000 字节作为整个文件。 虽然你是对的,但上下文会很有趣,甚至会有所帮助,但我认为我故意不提供任何内容,因为我真的对解决方案的纯学术探索感兴趣。【参考方案7】:

我认为这与计算机无关,而是你们如何编写日志记录类。你把最后的 500k 读成一个字符串对我来说听起来很奇怪,你为什么要这样做?

只需附加到日志文件。

  fstream myfile;
  myfile.open("test.txt",ios::app);

【讨论】:

我们正在截断文件以防止它变得太大。 所以当应用程序启动时,我们会截断日志,然后从那时起追加。 这里有一个想法 - 每次启动一个新的日志文件。那么你就有了不会被覆盖的历史日志。您还可以删除最旧的日志(例如保留最后 3 个),您将进入日志天堂。 是的,一个很好的建议,我玩弄了这个。我们可能会这样做。但我有保留,例如。很难描述人们邮寄多个文件的步骤。我们需要保留 x 天的日志而不是 x 日志。由于有些人不断重启应用程序。 另一种方法是丢失您可能想要的数据,即使您截断了日志文件。每天重新启动日志或应用程序重新启动时要容易得多。您可以根据需要保留尽可能多的日志(天或行),并且邮件日志很容易 - 邮件都称为“mylog_yyyy_mm_dd_n.log”。简单而高效。【参考方案8】:

Widefinder 2 has a lot of talk about efficient IO available(或者更准确地说,“备注”列下的链接有很多关于可用高效 IO 的信息)。

回答你的问题:

    (标题)使用 [标准库] 从文件中删除前 500,000 个字节

标准库在文件系统操作方面有些限制。如果您不限于标准库,您可以很容易地过早结束文件(也就是说,说“此点之后的所有内容都不再是该文件的一部分”),但很难晚启动文件(“所有内容在此之前不再是此文件的一部分”)。

简单地在文件中寻找 500,000 字节然后开始缓冲复制到新文件会很有效。但是一旦你这样做了,标准库就没有现成的“重命名这个文件”功能。本机操作系统函数可以高效地重命名文件,Boost.Filesystem 或 STLSoft 也可以。

    (实际问题)我们的日志记录类在初始化时,会在文件结尾之前寻找 500,000 个字节,将其余部分复制到 std::string,然后将其写回文件。

在这种情况下,您将删除文件的最后一位,并且在标准库之外很容易做到这一点。只需使用文件系统操作将文件大小设置为 500,000 字节(例如,ftruncateSetEndOfFile)。之后的任何内容都将被忽略。

【讨论】:

好消息,TR2 最终将拥有类似 boost.filesystem 的功能。

以上是关于使用 STL 从文件中删除除最后 500,000 个字节之外的所有字节的主要内容,如果未能解决你的问题,请参考以下文章

在stl容器中移除元素的恒定时间

从文本中删除最后一列(pdb文件保留其原始格式

删除 bash 脚本文件中除最后一次出现的重复变量

在迭代时从地图(或任何其他STL容器)中删除/删除内容

如何从表中删除除前两个和最后一个之外的所有行?

从一个连接到另一个表 SQL 的表中删除记录