diff --git a/include/platformstl/filesystem/file_lines.hpp b/include/platformstl/filesystem/file_lines.hpp index b8574eab..7a8c0022 100644 --- a/include/platformstl/filesystem/file_lines.hpp +++ b/include/platformstl/filesystem/file_lines.hpp @@ -4,7 +4,7 @@ * Purpose: Platform header for the file_lines components. * * Created: 25th October 2007 - * Updated: 20th March 2025 + * Updated: 23rd September 2026 * * Home: http://stlsoft.org/ * @@ -48,8 +48,8 @@ #ifndef STLSOFT_DOCUMENTATION_SKIP_SECTION # define PLATFORMSTL_VER_PLATFORMSTL_FILESYSTEM_HPP_FILE_LINES_MAJOR 2 # define PLATFORMSTL_VER_PLATFORMSTL_FILESYSTEM_HPP_FILE_LINES_MINOR 1 -# define PLATFORMSTL_VER_PLATFORMSTL_FILESYSTEM_HPP_FILE_LINES_REVISION 1 -# define PLATFORMSTL_VER_PLATFORMSTL_FILESYSTEM_HPP_FILE_LINES_EDIT 52 +# define PLATFORMSTL_VER_PLATFORMSTL_FILESYSTEM_HPP_FILE_LINES_REVISION 4 +# define PLATFORMSTL_VER_PLATFORMSTL_FILESYSTEM_HPP_FILE_LINES_EDIT 55 #endif /* !STLSOFT_DOCUMENTATION_SKIP_SECTION */ /** \file platformstl/filesystem/file_lines.hpp @@ -106,6 +106,9 @@ #ifndef STLSOFT_INCL_STLSOFT_STRING_HPP_SIMPLE_STRING # include #endif /* !STLSOFT_INCL_STLSOFT_STRING_HPP_SIMPLE_STRING */ +#ifndef STLSOFT_INCL_STLSOFT_UTIL_HPP_MINMAX +# include +#endif /* !STLSOFT_INCL_STLSOFT_UTIL_HPP_MINMAX */ #ifndef STLSOFT_INCL_ALGORITHM # define STLSOFT_INCL_ALGORITHM @@ -196,7 +199,6 @@ class basic_file_lines /// Creates an empty instance basic_file_lines() : m_mmf() - , m_contents() , m_strings() {} /// Creates an instance from the (contents of) the given path @@ -204,7 +206,6 @@ class basic_file_lines ss_explicit_k basic_file_lines(S const& path) : m_mmf() - , m_contents() , m_strings() { create_(path); @@ -217,7 +218,6 @@ class basic_file_lines /// Move the basic_file_lines(class_type&& rhs) STLSOFT_NOEXCEPT : m_mmf(std::move(rhs.m_mmf)) - , m_contents(std::move(rhs.m_contents)) , m_strings(std::move(rhs.m_strings)) {} #endif /* STLSOFT_CF_RVALUE_REFERENCES_SUPPORT */ @@ -389,37 +389,26 @@ class basic_file_lines size_t cch = static_cast(cb / sizeof(char_type)); #endif /* STLSOFT_CF_EXCEPTION_SUPPORT */ - // check if it looks like a binary file - if (base + cch != std::find(base, base + cch, '\0')) + if (0u == cb) { -#ifdef STLSOFT_CF_EXCEPTION_SUPPORT - STLSOFT_THROW_X(invalid_file_type_exception("file is binary (or unsupported text encoding)", 0, path)); -#else /* STLSOFT_CF_EXCEPTION_SUPPORT */ return; -#endif /* STLSOFT_CF_EXCEPTION_SUPPORT */ } - // 2. Create the contents string - - m_contents = base_string_type_(base, cch); - - STLSOFT_ASSERT(cch == m_contents.size()); + // 2. Parse the file, and populate the strings collection - // 3. Parse the file, and populate the strings collection + size_t const numReserved = maximum(static_cast(128), 1u + (cch / 10u)); - m_strings.reserve(1u + (cch / 10u)); + m_strings.reserve(numReserved); // This can work with EOL of CRLF or of LF, or a combination of the // two. - char_type const* const base1 = m_contents.data(); + char_type const* const base1 = base; char_type const* begin = base1; char_type const* const end = begin + cch; char_type const* s0 = begin; char_type prev = '\0'; -// bool const hasLF = (base + cch != std::find(base, base + cch, '\n')); -#if 1 { for (; begin != end; ++begin) { char_type const c = *begin; @@ -427,12 +416,29 @@ class basic_file_lines switch (c) { + case '\0': + + // reject if it looks like a binary file + +#ifdef STLSOFT_CF_EXCEPTION_SUPPORT + + STLSOFT_THROW_X(invalid_file_type_exception("file is binary (or unsupported text encoding)", 0, path)); +#else /* STLSOFT_CF_EXCEPTION_SUPPORT */ + + return; +#endif /* STLSOFT_CF_EXCEPTION_SUPPORT */ case '\r': if ('\r' == prev) { --eol; - m_strings.push_back(value_string_type_(s0, eol)); +#if __cplusplus >= 201103L + + m_strings.emplace_back(value_string_type_(s0, static_cast(eol - s0))); +#else /* ? C++ 11+ */ + + m_strings.push_back(value_string_type_(s0, static_cast(eol - s0))); +#endif /* C++ 11+ */ s0 = begin; } @@ -443,7 +449,13 @@ class basic_file_lines --eol; } - m_strings.push_back(value_string_type_(s0, eol)); +#if __cplusplus >= 201103L + + m_strings.emplace_back(value_string_type_(s0, static_cast(eol - s0))); +#else /* ? C++ 11+ */ + + m_strings.push_back(value_string_type_(s0, static_cast(eol - s0))); +#endif /* C++ 11+ */ s0 = begin + 1; break; @@ -452,7 +464,13 @@ class basic_file_lines { --eol; - m_strings.push_back(value_string_type_(s0, eol)); +#if __cplusplus >= 201103L + + m_strings.emplace_back(value_string_type_(s0, static_cast(eol - s0))); +#else /* ? C++ 11+ */ + + m_strings.push_back(value_string_type_(s0, static_cast(eol - s0))); +#endif /* C++ 11+ */ s0 = begin; } @@ -470,9 +488,14 @@ class basic_file_lines --eol; } - m_strings.push_back(value_string_type_(s0, eol)); +#if __cplusplus >= 201103L + + m_strings.emplace_back(value_string_type_(s0, static_cast(eol - s0))); +#else /* ? C++ 11+ */ + + m_strings.push_back(value_string_type_(s0, static_cast(eol - s0))); +#endif /* C++ 11+ */ } -#endif // Now determine whether we require the ongoing presence of the // underlying mapping. We can discard it if: @@ -514,9 +537,8 @@ class basic_file_lines } private: // fields - HRW_Ref_type m_mmf; - base_string_type_ m_contents; - strings_type_ m_strings; + HRW_Ref_type m_mmf; + strings_type_ m_strings; }; diff --git a/test/performance/CMakeLists.txt b/test/performance/CMakeLists.txt index 1e025c0e..e107cf58 100644 --- a/test/performance/CMakeLists.txt +++ b/test/performance/CMakeLists.txt @@ -1,6 +1,7 @@ if(X_CMAKE_CXX_FULLSTANDARD GREATER_EQUAL 2011) + add_subdirectory(test.performance.platformstl.file_lines) add_subdirectory(test.performance.platformstl.stopwatch) endif() diff --git a/test/performance/test.performance.platformstl.file_lines/CMakeLists.txt b/test/performance/test.performance.platformstl.file_lines/CMakeLists.txt new file mode 100644 index 00000000..d76ef1eb --- /dev/null +++ b/test/performance/test.performance.platformstl.file_lines/CMakeLists.txt @@ -0,0 +1,2 @@ +# SIS:AUTO_GENERATED: Remove this line if you edit the file, otherwise it will be overwritten +define_example_program(test.performance.platformstl.file_lines main.cpp) diff --git a/test/performance/test.performance.platformstl.file_lines/main.cpp b/test/performance/test.performance.platformstl.file_lines/main.cpp new file mode 100644 index 00000000..fe5c8916 --- /dev/null +++ b/test/performance/test.performance.platformstl.file_lines/main.cpp @@ -0,0 +1,433 @@ +/* ///////////////////////////////////////////////////////////////////////// + * File: test.performance.platformstl.file_lines/main.cpp + * + * Purpose: Comparative perf-test for platformstl::file_lines and + * std::ifstream + std::getline. + * + * Created: 23rd September 2026 + * Updated: 23rd September 2026 + * + * ////////////////////////////////////////////////////////////////////// */ + + +/* ///////////////////////////////////////////////////////////////////////// + * includes + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#if __cplusplus >= 201703L +# include +#endif /* C++17+ */ +#include +#include + + +/* ///////////////////////////////////////////////////////////////////////// + * types + */ + +typedef stlsoft::std_chrono_hrc_stopwatch stopwatch_t; +typedef stopwatch_t::interval_type interval_t; + +typedef std::pair< + interval_t +, std::size_t +> result_t; + +typedef platformstl::basic_file_lines< + char +, std::string +> file_lines_std_string_t; +typedef platformstl::basic_file_lines< + char +, stlsoft::simple_string +> file_lines_stlsoft_simple_string_t; +typedef platformstl::basic_file_lines< + char +, stlsoft::string_view +> file_lines_stlsoft_string_view_t; +#if __cplusplus >= 201703L + +typedef platformstl::basic_file_lines< + char +, std::string_view +> file_lines_std_string_view_t; +#endif /* C++17+ */ + + +/* ///////////////////////////////////////////////////////////////////////// + * constants + */ + +namespace { + +char const TEST_FILE_NAME[] = "test.performance.platformstl.file_lines.txt"; + +std::size_t const COLUMN_WIDTH_ANCHOR = 12; +std::size_t const COLUMN_WIDTH_IMPLEMENTATION = 48; +std::size_t const COLUMN_WIDTH_ITERATIONS = 12; +std::size_t const COLUMN_WIDTH_NS_PER_OPERATION = 12; +std::size_t const COLUMN_WIDTH_SCENARIO = 24; +std::size_t const COLUMN_WIDTH_TOTAL_TIME = 16; +const std::size_t NUM_ITERATIONS = 1000; +const std::size_t NUM_WARMUPS = 2; + +} // anonymous namespace + + +/* ///////////////////////////////////////////////////////////////////////// + * helpers + */ + +enum line_ending_t +{ + line_ending_lf = 0 + , line_ending_crlf + , line_ending_cr +}; + +std::string +make_line( + std::size_t length +) +{ + std::string line(length, 'x'); + + for (std::size_t i = 0; length != i; ++i) + { + line[i] = static_cast('a' + (i % 26)); + } + + return line; +} + +char const* +line_ending_name_( + line_ending_t ending +) +{ + switch (ending) + { + case line_ending_cr: + + return "CR"; + case line_ending_crlf: + + return "CRLF"; + case line_ending_lf: + default: + + return "LF"; + } +} + +bool +write_test_file( + std::size_t num_lines +, std::size_t line_length +, line_ending_t ending +) +{ + char const* eol = "\n"; + std::size_t eol_len = 1u; + + switch (ending) + { + case line_ending_cr: + + eol = "\r"; + break; + case line_ending_crlf: + + eol = "\r\n"; + eol_len = 2u; + break; + case line_ending_lf: + default: + + break; + } + + std::string const line = make_line(line_length); + + FILE* stm = NULL; + + if (0 != STLSOFT_API_INTERNAL_stdio_fopen_m(TEST_FILE_NAME, "wb", &stm)) + { + return false; + } + + for (std::size_t i = 0; num_lines != i; ++i) + { + if (line.size() != fwrite(line.data(), 1u, line.size(), stm)) + { + fclose(stm); + return false; + } + if (eol_len != fwrite(eol, 1u, eol_len, stm)) + { + fclose(stm); + return false; + } + } + + return (0 == fclose(stm)); +} + +template +std::size_t +read_file_lines_() +{ + T_file_lines lines(TEST_FILE_NAME); + std::size_t anchor = lines.size(); + + for (typename T_file_lines::const_iterator i = lines.begin(); lines.end() != i; ++i) + { + anchor += (*i).size(); + } + + return anchor; +} + +template +result_t +time_( + std::size_t num_iterations +, F fn +) +{ + interval_t interval = 0; + std::size_t anchor = 0; + + for (std::size_t w = NUM_WARMUPS; 0 != w; --w) + { + stopwatch_t sw; + + anchor = 0; + sw.start(); + + for (std::size_t i = 0; num_iterations != i; ++i) + { + anchor += fn(); + } + + sw.stop(); + + if (1 == w) + { + interval = sw.get_nanoseconds(); + } + } + + return std::make_pair(interval, anchor); +} + +void +display_result( + char const* scenario +, char const* implementation +, std::size_t num_iterations +, result_t const& result +) +{ + std::cout + << std::left + << std::setw(COLUMN_WIDTH_SCENARIO) << scenario + << std::setw(COLUMN_WIDTH_IMPLEMENTATION) << implementation + << std::right + << std::setw(COLUMN_WIDTH_ITERATIONS) << num_iterations + << std::setw(COLUMN_WIDTH_TOTAL_TIME) << result.first + << std::setw(COLUMN_WIDTH_NS_PER_OPERATION) << std::fixed << std::setprecision(3) + << (static_cast(result.first) / num_iterations) + << std::setw(COLUMN_WIDTH_ANCHOR) << result.second + << std::endl; +} + +void +run_scenario( + std::size_t num_lines +, std::size_t line_length +, line_ending_t ending +) +{ + char scenario[64]; + + stlsoft::snprintf( + scenario + , sizeof(scenario) + , "%lu %s x %lu %s" + , static_cast(num_lines) + , (1u == num_lines) ? "line" : "lines" + , static_cast(line_length) + , line_ending_name_(ending) + ); + + if (!write_test_file(num_lines, line_length, ending)) + { + std::cerr + << "failed to write " + << TEST_FILE_NAME + << std::endl + ; + + return; + } + + result_t const getc = time_(NUM_ITERATIONS, []() -> std::size_t { + std::vector lines; + std::string line; + std::size_t anchor = 0; + FILE* stm = NULL; + + if (0 != STLSOFT_API_INTERNAL_stdio_fopen_m(TEST_FILE_NAME, "rb", &stm)) + { + return 0u; + } + + for (int ch = fgetc(stm); EOF != ch; ch = fgetc(stm)) + { + if ('\n' == ch) + { + anchor += line.size(); + lines.push_back(line); + line.clear(); + } + else + { + line.push_back(static_cast(ch)); + } + } + + if (0u != line.size()) + { + anchor += line.size(); + lines.push_back(line); + } + + fclose(stm); + + return lines.size() + anchor; + }); + + result_t const getline = time_(NUM_ITERATIONS, [ending]() -> std::size_t { + std::ifstream stm(TEST_FILE_NAME, std::ios::binary); + std::vector lines; + std::string line; + std::size_t anchor = 0; + char const delimiter = (line_ending_cr == ending) ? '\r' : '\n'; + + while (std::getline(stm, line, delimiter)) + { + if (line_ending_crlf == ending && !line.empty() && '\r' == line.back()) + { + line.pop_back(); + } + + anchor += line.size(); + lines.push_back(line); + } + + return lines.size() + anchor; + }); + + result_t const file_lines_std_string = time_(NUM_ITERATIONS, []() -> std::size_t { + return read_file_lines_(); + }); + + result_t const file_lines_stlsoft_simple_string = time_(NUM_ITERATIONS, []() -> std::size_t { + return read_file_lines_(); + }); + + result_t const file_lines_stlsoft_string_view = time_(NUM_ITERATIONS, []() -> std::size_t { + return read_file_lines_(); + }); + + display_result(scenario, "vector+getc", NUM_ITERATIONS, getc); + display_result(scenario, "vector+getline", NUM_ITERATIONS, getline); + display_result(scenario, "basic_file_lines", NUM_ITERATIONS, file_lines_std_string); + display_result(scenario, "basic_file_lines", NUM_ITERATIONS, file_lines_stlsoft_simple_string); + display_result(scenario, "basic_file_lines", NUM_ITERATIONS, file_lines_stlsoft_string_view); +#if __cplusplus >= 201703L + + result_t const file_lines_std_string_view = time_(NUM_ITERATIONS, []() -> std::size_t { + return read_file_lines_(); + }); + + display_result(scenario, "basic_file_lines", NUM_ITERATIONS, file_lines_std_string_view); +#endif /* C++17+ */ +} + + +/* ///////////////////////////////////////////////////////////////////////// + * main() + */ + +int main(int /*argc*/, char* /*argv*/[]) +{ + std::cout + << std::left + << std::setw(COLUMN_WIDTH_SCENARIO) << "scenario" + << std::setw(COLUMN_WIDTH_IMPLEMENTATION) << "implementation" + << std::right + << std::setw(COLUMN_WIDTH_ITERATIONS) << "iterations" + << std::setw(COLUMN_WIDTH_TOTAL_TIME) << "total-ns" + << std::setw(COLUMN_WIDTH_NS_PER_OPERATION) << "ns/op" + << std::setw(COLUMN_WIDTH_ANCHOR) << "anchor" + << std::endl + ; + + struct scenario_t + { + std::size_t num_lines; + std::size_t line_length; + line_ending_t ending; + }; + + /* LF, then CRLF, then CR. The two large shapes match the original LF + * runs. The small shapes (one line, a handful of lines, about 2KB) are + * the fixed-cost region a later small-file path has to beat. + */ + scenario_t const scenarios[] = + { + { 1000, 64, line_ending_lf, }, + { 1000, 64, line_ending_crlf, }, + { 1000, 64, line_ending_cr, }, + { 5000, 80, line_ending_lf, }, + { 5000, 80, line_ending_crlf, }, + { 5000, 80, line_ending_cr, }, + { 1, 16, line_ending_lf, }, + { 1, 16, line_ending_crlf, }, + { 1, 16, line_ending_cr, }, + { 8, 32, line_ending_lf, }, + { 8, 32, line_ending_crlf, }, + { 8, 32, line_ending_cr, }, + { 32, 64, line_ending_lf, }, + { 32, 64, line_ending_crlf, }, + { 32, 64, line_ending_cr, }, + }; + + { for (std::size_t i = 0; STLSOFT_NUM_ELEMENTS(scenarios) != i; ++i) + { + run_scenario(scenarios[i].num_lines, scenarios[i].line_length, scenarios[i].ending); + }} + + std::remove(TEST_FILE_NAME); + + return EXIT_SUCCESS; +} + + +/* ///////////////////////////// end of file //////////////////////////// */ +