Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ if(MSVC AND BUILD_SHARED_LIBS)
"$<TARGET_FILE_DIR:percent_encode>") # <--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 "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>")
target_include_directories(percent_decode PUBLIC "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/benchmarks>")
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..."
"$<TARGET_FILE:ada>" # <--this is in-file
"$<TARGET_FILE_DIR:percent_decode>") # <--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")
Expand All @@ -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..."
Expand Down
92 changes: 92 additions & 0 deletions benchmarks/percent_decode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#include <memory>

#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 <benchmark/benchmark.h>

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();
}
87 changes: 54 additions & 33 deletions src/unicode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -455,39 +455,6 @@ unsigned constexpr convert_hex_to_binary(const char c) noexcept {
return hex_to_binary_table[c - '0'];
}

std::string percent_decode(const std::string_view input, size_t first_percent) {
// next line is for safety only, we expect users to avoid calling
// percent_decode when first_percent is outside the range.
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++;
} else {
unsigned a = convert_hex_to_binary(pointer[1]);
unsigned b = convert_hex_to_binary(pointer[2]);
char c = static_cast<char>(a * 16 + b);
dest += c;
pointer += 3;
}
}
return dest;
}

// 0..15 for hex digits, 0xFF otherwise - validate and decode with two loads.
constexpr static std::array<uint8_t, 256> unhex_table = []() consteval {
std::array<uint8_t, 256> t{};
Expand All @@ -504,6 +471,60 @@ constexpr static std::array<uint8_t, 256> unhex_table = []() consteval {
return t;
}();

std::string percent_decode(const std::string_view input, size_t first_percent) {
Comment thread
jbergstroem marked this conversation as resolved.
// next line is for safety only, we expect users to avoid calling
// percent_decode when first_percent is outside the range.
if (first_percent == std::string_view::npos) {
return std::string(input);
}

// 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 == '%') {
const uint8_t hi = unhex_table[static_cast<uint8_t>(p[1])];
const uint8_t lo = unhex_table[static_cast<uint8_t>(p[2])];
if ((hi | lo) >= 16) {
break;
}
*d++ = static_cast<char>((hi << 4) | lo);
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 {
const char* q = static_cast<const char*>(
std::memchr(p, '%', static_cast<size_t>(end - p)));
const char* run_end = q ? q : end;
const size_t n = static_cast<size_t>(run_end - p);
std::memcpy(d, p, n);
d += n;
p = run_end;
}
}

out.resize(static_cast<size_t>(d - d0));
return out;
}

std::string form_urlencoded_decode(const std::string_view input) {
const size_t len = input.size();
if (len == 0) [[unlikely]] {
Expand Down
Loading