fseek传递负偏移量和SEEK_CUR

8

我在一个非常大的文件中运行fseek(..)时,性能很差。 每次调用fseek函数时,我需要将文件指针位置向后移动100个字节

  fseek(fp, -100, SEEK_CUR);

之前,我在做这个:

  fseek(fp, (index)*100, SEEK_SET); // which makes basically the same...

我的问题是fseek如何通过文件移动指针并将文件指针设置在特定位置。
我曾认为它将获取文件指针并将其向后移动,但现在我认为它实际上是:
- 获取当前位置(cp) - 添加负索引(p = idx + cp) - 并将文件指针从文件开头移动到该位置(fseek(fp, p, SEEK_SET))

开放是什么?是“a+b”还是“r+b”……我猜不是“a+b”,也不是“append”。 - abRao
底层文件系统的性能可能是相关的。 - Martin James
由于其他原因,我无法使用追加模式打开文件,因此需要使用“r+b”模式打开它。 - ATL
在这种模式下,fseek是否有效? - ameyCU
你所描述的位置计算方式符合标准规范,但标准并未规定实现应如何将流设置到该位置。如果实现按照你的建议执行,我会感到惊讶,但这是允许的。 - John Bollinger
一般来说,在依赖fseek()之前,你应该三思而后行。有时它是完成任务的正确工具,但要注意并非所有流都是可寻址的。如果你总是在向后移动/查找恰好100个字节,那么请考虑在内存中缓冲至少这么多的先前数据,以便你不必寻找回去。 - John Bollinger
2个回答

5
首先,您使用的操作系统是什么?如果是Linux,请在strace下运行应用程序以查看它实际进行的系统调用。
其次,fopen() / fseek() / fread()不适合此访问模式。这些调用缓冲文件读取 - 通过向前读取。这对您没有任何好处。您可以fseek()到偏移量X,现在缓冲的任何数据都是无用的,您可以fread()100字节,并且缓冲的fread()会读取更多 - 可能是8 kB。您几乎在80次内读取文件的每个字节。您可以使用setbuf()setvbuf()来禁用缓冲,但然后您将进行100字节的读取,同时向后遍历文件。这应该会更快,但不如您可以去的那么快。
要尽可能快地完成此操作(而不涉及多线程和/或异步IO):
1. 使用open() / pread()。您不需要寻找 - pread()直接从任意偏移量读取。 2. 读取较大的块 - 比如8192 x 100。或者更大。像之前一样向后读取,但自己进行缓冲并从文件中的偏移量开始,该偏移量是您正在读取的大型大小的倍数 - 第一次读取可能少于819,200字节。首先处理缓冲区中的最后100个字节,然后向后处理缓冲区。当您处理了缓冲区中的前100个字节时,请使用pread()从文件中读取以前的819,200个字节(甚至更大)。 3. 如果可用,请使用直接IO。文件系统优化可能会尝试通过向前读取并将数据放入页面高速缓存来“优化”您的访问 - 您已经处理过的数据。因此,如果可能,请绕过页面高速缓存(不是所有操作系统都支持直接IO,也不是在支持直接IO的操作系统上的所有文件系统都实现它)。
类似于这样:
#define DATA_SIZE 100
#define NUM_CHUNKS (32UL * 1024UL)
#define READ_SIZE ( ( size_t ) DATA_SIZE * NUM_CHUNKS )

void processBuffer( const char *buffer, ssize_t bytes )
{
    if ( bytes <= 0 ) return;
    // process a buffer backwards...
}

void processFile( const char *filename )
{
    struct stat sb;
    // get page-aligned buffer for direct IO
    char *buffer = valloc( READ_SIZE );
    // Linux-style direct IO
    int fd = open( filename, O_RDONLY | O_DIRECT );
    fstat( fd, &sb );    
    // how many read operations?
    // use lldiv() to get quotient and remainder in one op
    lldiv_t numReads = lldiv( sb.st_size, READ_SIZE );
    if ( numReads.rem )
    {
        numReads.quot++;
    }
    while ( numReads.quot > 0 )
    {
        numReads.quot--;
        ssize_t bytesRead = pread( fd, buffer,
            READ_SIZE, numReads.quot * READ_SIZE );
        processBuffer( buffer, bytesRead );
    }
    free( buffer );
    close( fd );
}

您需要为此添加错误处理。

3
在用户应用程序层面,您可以将文件视为一个大的内存块,并像简单的内存操作一样移动文件指针(增加或减少指针以到达文件中所需的偏移量)。
但是,在运行时库和操作系统级别上,情况完全不同。处理文件的运行时库代码并未将整个文件内容加载到内存中。也许文件非常大,您只需要从中读取几个字节,有很多原因。
运行时库(以及由操作系统管理的文件缓存)仅将一些数据从文件加载到内存缓冲区中。您使用该数据(读取它,写入它),当您想要访问尚未加载到缓冲区中的信息时,文件管理代码会为您加载它;也许它会扩大缓冲区,也许它会将缓冲区写入文件(如果被修改)或者只是丢弃先前加载的数据(如果未被修改)并将另一块数据加载到缓冲区中。
当你使用fseek()跳转到文件的不同部分时,通常文件指针到达的区域尚未在内存中。我猜它从文件指针的新位置开始加载数据(在操作系统级别上,文件缓存以多个磁盘块的倍数加载数据)。因为你通过文件向后运行,我猜文件指针的新位置的数据几乎从未在内存中加载。这会触发磁盘访问,使其变慢。
我认为对你来说最好的解决方案是使用操作系统提供的函数将文件映射到内存中。在Linux(和可能在OSX)上阅读有关mmap()的内容或在Windows上阅读文件映射。它可能会对你有所帮助,但由于你使用的特定访问模式,改进可能不会显着。大多数情况下,程序按顺序从头到尾读取文件,并且处理文件和磁盘访问的代码已经针对此模式进行了优化。

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