Skip to content

Speed up cimg::fsize#484

Merged
dtschump merged 1 commit into
GreycLab:developfrom
apocelipes:feat-optimize-fsize
Jul 1, 2026
Merged

Speed up cimg::fsize#484
dtschump merged 1 commit into
GreycLab:developfrom
apocelipes:feat-optimize-fsize

Conversation

@apocelipes

Copy link
Copy Markdown
Contributor

In a POSIX environment, there is a faster way to retrieve a file's size that does not modify the file offset.

This can roughly improve fsize performance by 3x–10x

Benchmark code:

// A 1 MiB file. However size does not matter here.
static const char* filePath = "test.bin";

typedef int64_t cimg_int64;

inline cimg_int64 fsize(std::FILE *file) {
    if (!file) return (cimg_int64)-1;
    const long pos = std::ftell(file);
    std::fseek(file,0,SEEK_END);
    const cimg_int64 siz = (cimg_int64)std::ftell(file);
    std::fseek(file,pos,SEEK_SET);
    return siz;
}
inline cimg_int64 fsize(const char *const filename) {
    std::FILE *const file = std::fopen(filename,"rb");
    const cimg_int64 siz = fsize(file);
    std::fclose(file);
    return siz;
}

inline cimg_int64 fsizeOptimize(std::FILE *file) {
    if (!file) return (cimg_int64)-1;
    int fd = fileno(file);
    struct stat st;
    if (fstat(fd, &st) < 0) return -1;
    return (cimg_int64)st.st_size;
}
inline cimg_int64 fsizeOptimize(const char *const filename) {
    struct stat st;
    if (stat(filename, &st) < 0) return -1;
    return (cimg_int64)st.st_size;
}

static void BM_fsize_FILE(benchmark::State& state) {
    FILE* fp = std::fopen(filePath, "rb");
    for (auto _ : state) {
        benchmark::DoNotOptimize(fsize(fp));
        benchmark::ClobberMemory();
    }
    std::fclose(fp);
}
BENCHMARK(BM_fsize_FILE);
static void BM_fsize_filename(benchmark::State& state) {
    for (auto _ : state) {
        benchmark::DoNotOptimize(fsize(filePath));
        benchmark::ClobberMemory();
    }
}
BENCHMARK(BM_fsize_filename);

static void BM_fsizeOptimize_FILE(benchmark::State& state) {
    FILE* fp = std::fopen(filePath, "rb");
    for (auto _ : state) {
        benchmark::DoNotOptimize(fsizeOptimize(fp));
        benchmark::ClobberMemory();
    }
    std::fclose(fp);
}
BENCHMARK(BM_fsizeOptimize_FILE);
static void BM_fsizeOptimize_filename(benchmark::State& state) {
    for (auto _ : state) {
        benchmark::DoNotOptimize(fsizeOptimize(filePath));
        benchmark::ClobberMemory();
    }
}
BENCHMARK(BM_fsizeOptimize_filename);

Results:

# macOS
Run on (10 X 24 MHz CPU s)
CPU Caches:
  L1 Data 64 KiB
  L1 Instruction 128 KiB
  L2 Unified 4096 KiB (x10)
Load Average: 2.10, 2.08, 1.88
--------------------------------------------------------------------
Benchmark                          Time             CPU   Iterations
--------------------------------------------------------------------
BM_fsize_FILE                    604 ns          604 ns      1196254
BM_fsize_filename               6182 ns         6182 ns       111123
BM_fsizeOptimize_FILE            227 ns          227 ns      3107534
BM_fsizeOptimize_filename        405 ns          405 ns      1726758

# Linux
Run on (4 X 48 MHz CPU s)
Load Average: 0.13, 0.07, 0.02
--------------------------------------------------------------------
Benchmark                          Time             CPU   Iterations
--------------------------------------------------------------------
BM_fsize_FILE                    371 ns          370 ns      1871449
BM_fsize_filename               1213 ns         1208 ns       566134
BM_fsizeOptimize_FILE            141 ns          140 ns      4872801
BM_fsizeOptimize_filename        199 ns          198 ns      3489223

@apocelipes apocelipes changed the base branch from master to develop June 30, 2026 10:29
@apocelipes apocelipes force-pushed the feat-optimize-fsize branch from c6e5e82 to 2131624 Compare July 1, 2026 06:24
@apocelipes

Copy link
Copy Markdown
Contributor Author

Conflicts are resolved.

@apocelipes apocelipes force-pushed the feat-optimize-fsize branch from 2131624 to 4c10edc Compare July 1, 2026 08:25
@apocelipes

Copy link
Copy Markdown
Contributor Author

The optimization path for Windows has also been added.

@dtschump

dtschump commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

I've included/rewritten your patch, with a few changes for better portability :

    //! Get file size.
    /**
       \param file Specified file to get size from.
       \return File size or '-1' if file does not exist.
    **/
    inline cimg_int64 fsize(std::FILE *const file) {
      if (!file) return (cimg_int64)-1;

#if cimg_OS==1 // Optimized for POSIX Environements (Linux, macOS, BSD, etc.)
      const int fd = fileno(file);
      struct stat st;
      if (fd>=0 && !fstat(fd,&st)) return (cimg_int64)st.st_size;
#elif cimg_OS==2 // Optimized for Windows Environements (MSVC, MinGW)
      const int fd = _fileno(file);
      struct _stati64 st;
      if (fd>=0 && !_fstati64(fd,&st)) return (cimg_int64)st.st_size;
#endif

      // Fallback, used for non-POSIX systems or if fstat failed (pipes or sockets).
      const cimg_long pos = cimg::ftell(file);
      cimg::fseek(file, 0, SEEK_END);
      const cimg_int64 siz = (cimg_int64)cimg::ftell(file);
      cimg::fseek(file, pos, SEEK_SET);
      return siz;
    }

    //! Get file size from filename.
    /**
       \param filename Specified filename to get size from.
       \return File size or '-1' if file does not exist.
    **/
    inline cimg_int64 fsize(const char *const filename) {
      if (!filename || !*filename) return (cimg_int64)-1;

#if cimg_OS==1 // Optimized for POSIX Environements (Linux, macOS, BSD, etc.)
      struct stat st;
      if (!stat(filename,&st)) return (cimg_int64)st.st_size;
#elif cimg_OS==2 // Optimized for Windows Environements (MSVC, MinGW)
      // Convert UTF-8 path to wchar_t.
      int len = MultiByteToWideChar(CP_UTF8,0,filename,-1,0,0);
      if (len>0) {
        CImg<wchar_t> wfilename((unsigned int)len);
        if (MultiByteToWideChar(CP_UTF8,0,filename,-1,wfilename,len)) {
          struct _stati64 st;
          if (!_wstati64(wfilename,&st)) return (cimg_int64)st.st_size;
        }
      }
#endif

      // Fallback.
      std::FILE *const file = cimg::std_fopen(filename, "rb");
      if (!file) return (cimg_int64)-1;
      const cimg_int64 siz = cimg::fsize(file);
      cimg::fclose(file);
      return siz;
    }

What do you think ?

@apocelipes apocelipes force-pushed the feat-optimize-fsize branch from 4c10edc to 72b009f Compare July 1, 2026 09:00
@apocelipes

Copy link
Copy Markdown
Contributor Author

I've included/rewritten your patch, with a few changes for better portability :

    //! Get file size.
    /**
       \param file Specified file to get size from.
       \return File size or '-1' if file does not exist.
    **/
    inline cimg_int64 fsize(std::FILE *const file) {
      if (!file) return (cimg_int64)-1;

#if cimg_OS==1 // Optimized for POSIX Environements (Linux, macOS, BSD, etc.)
      const int fd = fileno(file);
      struct stat st;
      if (fd>=0 && !fstat(fd,&st)) return (cimg_int64)st.st_size;
#elif cimg_OS==2 // Optimized for Windows Environements (MSVC, MinGW)
      const int fd = _fileno(file);
      struct _stati64 st;
      if (fd>=0 && !_fstati64(fd,&st)) return (cimg_int64)st.st_size;
#endif

      // Fallback, used for non-POSIX systems or if fstat failed (pipes or sockets).
      const cimg_long pos = cimg::ftell(file);
      cimg::fseek(file, 0, SEEK_END);
      const cimg_int64 siz = (cimg_int64)cimg::ftell(file);
      cimg::fseek(file, pos, SEEK_SET);
      return siz;
    }

    //! Get file size from filename.
    /**
       \param filename Specified filename to get size from.
       \return File size or '-1' if file does not exist.
    **/
    inline cimg_int64 fsize(const char *const filename) {
      if (!filename || !*filename) return (cimg_int64)-1;

#if cimg_OS==1 // Optimized for POSIX Environements (Linux, macOS, BSD, etc.)
      struct stat st;
      if (!stat(filename,&st)) return (cimg_int64)st.st_size;
#elif cimg_OS==2 // Optimized for Windows Environements (MSVC, MinGW)
      // Convert UTF-8 path to wchar_t.
      int len = MultiByteToWideChar(CP_UTF8,0,filename,-1,0,0);
      if (len>0) {
        CImg<wchar_t> wfilename((unsigned int)len);
        if (MultiByteToWideChar(CP_UTF8,0,filename,-1,wfilename,len)) {
          struct _stati64 st;
          if (!_wstati64(wfilename,&st)) return (cimg_int64)st.st_size;
        }
      }
#endif

      // Fallback.
      std::FILE *const file = cimg::std_fopen(filename, "rb");
      if (!file) return (cimg_int64)-1;
      const cimg_int64 siz = cimg::fsize(file);
      cimg::fclose(file);
      return siz;
    }

What do you think ?

@dtschump That's great! Additionally, fstat itself checks whether the file descriptor (fd) is valid, so this eliminates the need for a separate conditional branch. Given the typical use cases for CImg, it is highly unlikely that the obtained file descriptor (fd) would be invalid. So I removed that check in the new commit.

@dtschump

dtschump commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

OK, I've tested on Windows, and it does not work. I had to do other modifications to make it work.

@apocelipes apocelipes force-pushed the feat-optimize-fsize branch from 72b009f to ee49a5e Compare July 1, 2026 09:57
@apocelipes

Copy link
Copy Markdown
Contributor Author

I resolved the conflict. _filelengthi64 also no need to check fd so I removed the "if (fd>=0)"

@apocelipes apocelipes force-pushed the feat-optimize-fsize branch from ee49a5e to ee6abc9 Compare July 1, 2026 12:58
@apocelipes

Copy link
Copy Markdown
Contributor Author

@dtschump I fixed Windows code. Please take a look.

@apocelipes

Copy link
Copy Markdown
Contributor Author

P.S. I don't really recommend using the POSIX-compatible functions provided by crt; they're slower than the native API and behave differently from POSIX.

@dtschump dtschump merged commit ddcaaf0 into GreycLab:develop Jul 1, 2026
@apocelipes apocelipes deleted the feat-optimize-fsize branch July 1, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants