diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 72c0249d1..3293c9bbb 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -68,6 +68,19 @@ if(MSVC AND BUILD_SHARED_LIBS) "$") # <--this is out-file path endif() +# Percent Decode +add_executable(percent_decode percent_decode.cpp) +target_link_libraries(percent_decode PRIVATE ada counters::counters) +target_include_directories(percent_decode PUBLIC "$") +target_include_directories(percent_decode PUBLIC "$") +if(MSVC AND BUILD_SHARED_LIBS) + # Copy the ada dll into the directory + add_custom_command(TARGET percent_decode POST_BUILD # Adds a post-build event + COMMAND ${CMAKE_COMMAND} -E copy_if_different # which executes "cmake -E copy_if_different..." + "$" # <--this is in-file + "$") # <--this is out-file path +endif() + add_executable(model_bench model_bench.cpp) target_link_libraries(model_bench PRIVATE ada counters::counters) target_compile_definitions(model_bench PRIVATE ADA_URL_FILE="${url-dataset_SOURCE_DIR}/out.txt") @@ -78,10 +91,11 @@ target_link_libraries(benchdata PRIVATE benchmark::benchmark) target_link_libraries(bbc_bench PRIVATE benchmark::benchmark) target_link_libraries(bench_ipv4 PRIVATE benchmark::benchmark) target_link_libraries(percent_encode PRIVATE benchmark::benchmark) +target_link_libraries(percent_decode PRIVATE benchmark::benchmark) target_link_libraries(bench_search_params PRIVATE benchmark::benchmark) target_link_libraries(urlpattern PRIVATE benchmark::benchmark) -set(BENCHMARKS wpt_bench bench benchdata bbc_bench bench_ipv4 percent_encode bench_search_params urlpattern) +set(BENCHMARKS wpt_bench bench benchdata bbc_bench bench_ipv4 percent_encode percent_decode bench_search_params urlpattern) add_custom_target(run_all_benchmarks COMMAND ${CMAKE_COMMAND} -E echo "Running all benchmarks..." diff --git a/benchmarks/percent_decode.cpp b/benchmarks/percent_decode.cpp new file mode 100644 index 000000000..3fa873cf8 --- /dev/null +++ b/benchmarks/percent_decode.cpp @@ -0,0 +1,92 @@ +#include + +#include "ada.h" +#include "ada/character_sets.h" +#include "ada/unicode.h" +#include "counters/event_counter.h" +counters::event_collector collector; +size_t N = 1000; + +#include + +std::string examples[] = { + "utm_source=twc%20mobile&name=John%20Doe&ref=web-twc-ao-gbl", + "https%3A%2F%2Fexample.com%2Fpath%3Fa%3D1%26b%3D2%26c%3D3%26d%3D4", + "the/quick/brown/fox/jumps/over/the/lazy/dog%2Ehtml", + "caf%C3%A9%20%E4%BD%A0%E5%A5%BD%20%F0%9F%98%80%20done"}; + +void init_data() {} + +double examples_bytes = []() -> double { + size_t bytes{0}; + for (std::string& url_string : examples) { + bytes += url_string.size(); + } + return double(bytes); +}(); + +static void Decode(benchmark::State& state) { + for (auto _ : state) { + for (std::string& url_string : examples) { + benchmark::DoNotOptimize( + ada::unicode::percent_decode(url_string, url_string.find('%'))); + } + } + if (collector.has_events()) { + counters::event_aggregate aggregate{}; + for (size_t i = 0; i < N; i++) { + std::atomic_thread_fence(std::memory_order_acquire); + collector.start(); + for (std::string& url_string : examples) { + benchmark::DoNotOptimize( + ada::unicode::percent_decode(url_string, url_string.find('%'))); + } + std::atomic_thread_fence(std::memory_order_release); + counters::event_count allocate_count = collector.end(); + aggregate << allocate_count; + } + state.counters["instructions/url"] = + aggregate.best.instructions() / std::size(examples); + state.counters["instructions/cycle"] = + aggregate.total.instructions() / aggregate.total.cycles(); + state.counters["instructions/byte"] = + aggregate.best.instructions() / examples_bytes; + state.counters["GHz"] = + aggregate.total.cycles() / aggregate.total.elapsed_ns(); + } + state.counters["time/byte"] = benchmark::Counter( + examples_bytes, benchmark::Counter::kIsIterationInvariantRate | + benchmark::Counter::kInvert); + state.counters["time/url"] = + benchmark::Counter(double(std::size(examples)), + benchmark::Counter::kIsIterationInvariantRate | + benchmark::Counter::kInvert); + state.counters["speed"] = benchmark::Counter( + examples_bytes, benchmark::Counter::kIsIterationInvariantRate); + state.counters["url/s"] = + benchmark::Counter(double(std::size(examples)), + benchmark::Counter::kIsIterationInvariantRate); +} +BENCHMARK(Decode); + +int main(int argc, char** argv) { +#if defined(ADA_RUST_VERSION) + benchmark::AddCustomContext("rust version ", ADA_RUST_VERSION); +#endif +#if (__APPLE__ && __aarch64__) || defined(__linux__) + if (!collector.has_events()) { + benchmark::AddCustomContext("performance counters", + "No privileged access (sudo may help)."); + } +#else + if (!collector.has_events()) { + benchmark::AddCustomContext("performance counters", "Unsupported system."); + } +#endif + if (collector.has_events()) { + benchmark::AddCustomContext("performance counters", "Enabled"); + } + benchmark::Initialize(&argc, argv); + benchmark::RunSpecifiedBenchmarks(); + benchmark::Shutdown(); +} diff --git a/src/unicode.cpp b/src/unicode.cpp index 5899a4640..7389fc983 100644 --- a/src/unicode.cpp +++ b/src/unicode.cpp @@ -461,31 +461,51 @@ std::string percent_decode(const std::string_view input, size_t first_percent) { if (first_percent == std::string_view::npos) { return std::string(input); } - std::string dest; - dest.reserve(input.length()); - dest.append(input.substr(0, first_percent)); - const char* pointer = input.data() + first_percent; - const char* end = input.data() + input.size(); - // Optimization opportunity: if the following code gets - // called often, it can be optimized quite a bit. - while (pointer < end) { - const char ch = pointer[0]; - size_t remaining = end - pointer - 1; - if (ch != '%' || remaining < 2 || - ( // ch == '%' && // It is unnecessary to check that ch == '%'. - (!is_ascii_hex_digit(pointer[1]) || - !is_ascii_hex_digit(pointer[2])))) { - dest += ch; - pointer++; + + // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage) + const char* const src = input.data(); + const char* const end = src + input.size(); + + // Decoding never grows the string, so a single pre-sized buffer written via + // bulk memcpy of the plain runs (then shrunk to the final length) avoids the + // byte-at-a-time appends of the naive version. + std::string out(input.size(), '\0'); + char* d = out.data(); + char* const d0 = d; + + std::memcpy(d, src, first_percent); + d += first_percent; + + const char* p = src + first_percent; + while (p < end) { + if (*p == '%') { + // Decode runs of valid %XX tightly (common for nested/encoded URLs). + while (p + 2 < end && *p == '%') { + if (!is_ascii_hex_digit(p[1]) || !is_ascii_hex_digit(p[2])) { + break; + } + *d++ = static_cast(convert_hex_to_binary(p[1]) * 16 + + convert_hex_to_binary(p[2])); + p += 3; + } + if (p < end && *p == '%') { + // Not a valid escape (too few chars left or bad hex): copy '%' + // literally and keep scanning after it. + *d++ = *p++; + } } else { - unsigned a = convert_hex_to_binary(pointer[1]); - unsigned b = convert_hex_to_binary(pointer[2]); - char c = static_cast(a * 16 + b); - dest += c; - pointer += 3; + const char* q = static_cast( + std::memchr(p, '%', static_cast(end - p))); + const char* run_end = q ? q : end; + const size_t n = static_cast(run_end - p); + std::memcpy(d, p, n); + d += n; + p = run_end; } } - return dest; + + out.resize(static_cast(d - d0)); + return out; } // 0..15 for hex digits, 0xFF otherwise - validate and decode with two loads. diff --git a/tests/basic_tests.cpp b/tests/basic_tests.cpp index 0e2025fbb..e65a0f751 100644 --- a/tests/basic_tests.cpp +++ b/tests/basic_tests.cpp @@ -572,6 +572,26 @@ TEST(basic_tests, can_parse_consistency_percent_encoded_host) { } } +// ada::unicode::percent_decode +TEST(basic_tests, percent_decode_direct) { + using ada::unicode::percent_decode; + constexpr auto npos = std::string_view::npos; + // No percent sign: first_percent == npos returns the input unchanged. + ASSERT_EQ(percent_decode("no percent here", npos), "no percent here"); + ASSERT_EQ(percent_decode("", npos), ""); + // Valid escapes are decoded. + ASSERT_EQ(percent_decode("a%2Eb", 1), "a.b"); + ASSERT_EQ(percent_decode("%41%42%43", 0), "ABC"); + ASSERT_EQ(percent_decode("caf%C3%A9", 3), std::string("caf\xc3\xa9")); + // A plain run followed by an escape exercises the memchr run-copy. + ASSERT_EQ(percent_decode("hello%20world", 5), "hello world"); + // Invalid escapes are copied literally. + ASSERT_EQ(percent_decode("%zz", 0), "%zz"); // non-hex digits + ASSERT_EQ(percent_decode("x%2", 1), "x%2"); // truncated escape at end + ASSERT_EQ(percent_decode("100%", 3), "100%"); // trailing '%' + ASSERT_EQ(percent_decode("%%41", 0), "%A"); // '%' then a valid escape +} + // Regression: try_can_parse_absolute_fast returned true for a valid IPv4 host // without validating the port. For "wS://1.3.3.51.:+" the host "1.3.3.51." // passes the IPv4 fast path, but the port "+" is not a valid digit, so the