Skip to content

Commit

Permalink
Optimize file input/output operations (#139)
Browse files Browse the repository at this point in the history
File reading and wirting operations are optimized in terms of time and
opened file descriptors
  • Loading branch information
bugdea1er authored Jan 18, 2025
1 parent 655b42d commit b356fd4
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 27 deletions.
1 change: 1 addition & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Checks: >
readability-*,
-*-use-default-member-init,
-bugprone-easily-swappable-parameters,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-init-variables,
-cppcoreguidelines-pro-type-vararg,
-misc-const-correctness,
Expand Down
171 changes: 149 additions & 22 deletions src/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@

#include "utils.hpp"

#include <array>
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <ios>
#include <iterator>
#include <limits>
#include <sstream>
#include <string_view>
#include <system_error>
#include <utility>

#ifdef _WIN32
#define NOMINMAX
#define UNICODE
#include <Windows.h>
#else
Expand All @@ -23,6 +26,12 @@
namespace tmp {
namespace {

/// Maximum number of bytes in one read/write operation
constexpr std::string_view::size_type io_max = std::numeric_limits<int>::max();

/// A block size for file reading
constexpr std::size_t block_size = 4096;

/// Creates a temporary file with the given prefix in the system's
/// temporary directory, and opens it for reading and writing
/// @param label A label to attach to the temporary file path
Expand Down Expand Up @@ -62,6 +71,80 @@ create_file(std::string_view label, std::string_view extension) {

return std::pair(path, handle);
}

/// Reads the content from the given native handle
/// @note uses handle current offset for reading
/// @param[in] handle A native handle to read from
/// @param[out] ec Parameter for error reporting
/// @returns A string with read contents
/// @throws std::bad_alloc if memory allocation fails
std::string read(entry::native_handle_type handle, std::error_code& ec) {
std::array buffer = std::array<std::string::value_type, block_size>();
std::ostringstream content;

while (true) {
#ifdef _WIN32
DWORD read;
if (!ReadFile(handle, buffer.data(), block_size, &read, nullptr)) {
ec = std::error_code(GetLastError(), std::system_category());
return std::string();
}
#else
ssize_t read = ::read(handle, buffer.data(), block_size);
if (read < 0) {
ec = std::error_code(errno, std::system_category());
return std::string();
}
#endif
if (read == 0) {
break;
}

content.write(buffer.data(), read);
}

ec.clear();
return std::move(content).str();
}

/// Writes the given content to the native handle
/// @note uses handle current offset for writing
/// @param[in] handle A native handle to write to
/// @param[in] content A string to write to this file
/// @param[out] ec Parameter for error reporting
void write(entry::native_handle_type handle, std::string_view content,
std::error_code& ec) noexcept {
do {
int writable = static_cast<int>(std::min(content.size(), io_max));

#ifdef _WIN32
DWORD written;
if (!WriteFile(handle, content.data(), writable, &written, nullptr)) {
ec = std::error_code(GetLastError(), std::system_category());
return;
}
#else
ssize_t written = ::write(handle, content.data(), writable);
if (written < 0) {
ec = std::error_code(errno, std::system_category());
return;
}
#endif

content = content.substr(written);
} while (!content.empty());
ec.clear();

#ifdef _WIN32
if (!FlushFileBuffers(handle)) {
ec = std::error_code(GetLastError(), std::system_category());
}
#else
if (fsync(handle) == -1) {
ec = std::error_code(errno, std::system_category());
}
#endif
}
} // namespace

file::file(std::string_view label, std::string_view extension)
Expand Down Expand Up @@ -101,17 +184,36 @@ std::string file::read() const {
}

std::string file::read(std::error_code& ec) const {
// TODO: can be optimized to not open the file again using native API
#ifdef _WIN32 // TODO: can be optimized to not open the file again
if (!binary) {
try {
std::ifstream stream = input_stream();
stream.exceptions(std::ios::failbit | std::ios::badbit);

return std::string(std::istreambuf_iterator(stream), {});
} catch (const std::ios::failure& err) {
ec = err.code();
return std::string();
}
}
#endif
native_handle_type handle = native_handle();

try {
std::ifstream stream = input_stream();
stream.exceptions(std::ios::failbit | std::ios::badbit);
#ifdef _WIN32
if (SetFilePointer(handle, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER) {
ec = std::error_code(GetLastError(), std::system_category());
}
#else
if (lseek(handle, 0, SEEK_SET) == -1) {
ec = std::error_code(errno, std::system_category());
}
#endif

return std::string(std::istreambuf_iterator(stream), {});
} catch (const std::ios::failure& err) {
ec = err.code();
if (ec) {
return std::string();
}

return tmp::read(handle, ec);
}

void file::write(std::string_view content) const {
Expand All @@ -124,15 +226,21 @@ void file::write(std::string_view content) const {
}

void file::write(std::string_view content, std::error_code& ec) const {
// TODO: can be optimized to not open the file again using native API
native_handle_type handle = native_handle();

try {
std::ofstream stream = output_stream(std::ios::trunc);
stream.exceptions(std::ios::failbit | std::ios::badbit);
#ifdef _WIN32
if (SetFilePointer(handle, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER ||
!SetEndOfFile(handle)) {
ec = std::error_code(GetLastError(), std::system_category());
}
#else
if (ftruncate(handle, 0) == -1) {
ec = std::error_code(errno, std::system_category());
}
#endif

stream << content;
} catch (const std::ios::failure& err) {
ec = err.code();
if (!ec) {
append(content, ec);
}
}

Expand All @@ -146,15 +254,34 @@ void file::append(std::string_view content) const {
}

void file::append(std::string_view content, std::error_code& ec) const {
// TODO: can be optimized to not open the file again using native API
#ifdef _WIN32 // TODO: can be optimized to not open the file again
if (!binary) {
try {
std::ofstream stream = output_stream(std::ios::app);
stream.exceptions(std::ios::failbit | std::ios::badbit);

stream << content;
} catch (const std::ios::failure& err) {
ec = err.code();
}

return;
}
#endif
native_handle_type handle = native_handle();

try {
std::ofstream stream = output_stream(std::ios::app);
stream.exceptions(std::ios::failbit | std::ios::badbit);
#ifdef _WIN32
if (SetFilePointer(handle, 0, NULL, FILE_END) == INVALID_SET_FILE_POINTER) {
ec = std::error_code(GetLastError(), std::system_category());
}
#else
if (lseek(handle, 0, SEEK_END) == -1) {
ec = std::error_code(errno, std::system_category());
}
#endif

stream << content;
} catch (const std::ios::failure& err) {
ec = err.code();
if (!ec) {
tmp::write(handle, content, ec);
}
}

Expand Down
22 changes: 17 additions & 5 deletions tests/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ TEST(file, read_text) {
/// Tests file reading error reporting
TEST(file, read_error) {
file tmpfile = file();
fs::remove(tmpfile);

#ifdef _WIN32
CloseHandle(tmpfile.native_handle());
#else
::close(tmpfile.native_handle());
#endif

EXPECT_THROW(tmpfile.read(), fs::filesystem_error);
}
Expand Down Expand Up @@ -189,10 +194,13 @@ TEST(file, write_text) {
/// Tests file writing error reporting
TEST(file, write_error) {
file tmpfile = file();
fs::permissions(tmpfile.path(), fs::perms::none);
#ifdef _WIN32
CloseHandle(tmpfile.native_handle());
#else
::close(tmpfile.native_handle());
#endif

EXPECT_THROW(tmpfile.write("Hello!"), fs::filesystem_error);
fs::permissions(tmpfile.path(), fs::perms::all);
}

/// Tests binary file appending
Expand Down Expand Up @@ -250,10 +258,14 @@ TEST(file, append_text) {
/// Tests file appending error reporting
TEST(file, append_error) {
file tmpfile = file();
fs::permissions(tmpfile.path(), fs::perms::none);

#ifdef _WIN32
CloseHandle(tmpfile.native_handle());
#else
::close(tmpfile.native_handle());
#endif

EXPECT_THROW(tmpfile.append("world!"), fs::filesystem_error);
fs::permissions(tmpfile.path(), fs::perms::all);
}

/// Tests binary file reading from input_stream
Expand Down

0 comments on commit b356fd4

Please sign in to comment.