Open19

URLパーザのadaを使ってみる

togetoge

Node.jsが採用した高速なURLパーザ、adaをC++で使ってみようと思います。
数多あるURLパーザよりも本当に速いんだろうか…。

togetoge

他のURLパーザだとここらへんでしょうか。

  • cpp-netlib
  • lurlparser
  • skyr-url
  • Uriparser

私はいつもはskyr-urlを使っています。

togetoge

お試しするに際して、conanというパッケージマネージャーを使います。
C++のパッケージマネージャー嫌いな人、vcpkgみたいな他のパッケージマネージャー使っている人ごめんなさい。

togetoge

conanのメインのパッケージリポジトリであるconan-center-indexにはadaのパッケージがあるので、これを使います。

[requires]
ada/2.3.0

[generators]
CMakeToolchain
CMakeDeps
togetoge

conanfile.txtが存在する同じディレクトリで以下のコマンドを実行します。

conan install . -of build --build=missing
togetoge

同じディレクトリにCMakeLists.txtも作成します。

CMakeLists.txt
cmake_minimum_required(VERSION 3.9)
project(study_ada LANGUAGES CXX)

include(CheckIPOSupported)
check_ipo_supported(RESULT ipo_supported OUTPUT error)
if(ipo_supported)
    set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

find_package(ada REQUIRED CONFIG)

foreach(idx RANGE 1 99)
    if(idx LESS 10)
        set(idx "0${idx}")
    endif()

    if(EXISTS "${CMAKE_SOURCE_DIR}/test${idx}.cpp")
        add_executable(test${idx} test${idx}.cpp)
        target_link_libraries(test${idx} PRIVATE ada::ada)
        target_compile_features(test${idx} PRIVATE cxx_std_17)
    endif()
endforeach()

※cmakeのハイライトさせたいのですが、なんかada::adaの部分で表示が乱れるのでプレーンテキストにしています。

togetoge

test01.cppファイルも同じディレクトリに配置します。

test01.cpp
#include <ada.h>

auto main() -> int {
  auto const url = "https://github.com/ada-url/ada";
  if (auto const result = ada::parse<ada::url_aggregator>(url)) {
    std::cout << "host : " << result->get_host() << '\n';
  }

  return 0;
}
togetoge

cmakeでのビルドを実行します。

cmake -B build -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release -S .
cmake --build build -j 4
togetoge

build/test01が生成されます。
実行するとこんな感じ。

host : github.com
togetoge

他のメソッドも使ってみます。

test02.cpp
#include <ada.h>

auto main() -> int {
  auto const url = "https://github.com/ada-url/ada";
  if (auto const result = ada::parse<ada::url_aggregator>(url)) {
    std::cout << "protocol : " << result->get_protocol() << '\n';
    std::cout << "host     : " << result->get_host()     << '\n';
    std::cout << "path     : " << result->get_pathname() << '\n';
    std::cout << "port     : " << result->get_port()     << '\n';
    std::cout << '\n';
    std::cout << "href     : " << result->get_href()     << '\n';
  }

  return 0;
}

実行するとこんな感じ。
portについてはデフォルトポート番号が埋まるわけではないですね。

get_xxx()の戻り値はstd::string_viewでした。

protocol : https:
host     : github.com
path     : /ada-url/ada
port     : 

href     : https://github.com/ada-url/ada
togetoge

公式のREADME.mdには明記ありませんが、ada::parseのテンプレートパラメータは省略可能みたいです。

test03.cpp
#include <ada.h>

auto main() -> int {
  auto const url = "https://github.com/ada-url/ada";
  if (auto const result = ada::parse(url)) {
    std::cout << "host : " << result->get_host() << '\n';
  }

  return 0;
}

README.mdが省略表記ではないので、これ以降は省略しない方で書きますかね。

togetoge

「じゃあ、テンプレートパラメータは何のためにあるの?」ってなりますが内部で処理結果をどう持つのかに影響します。

クラス名 動作 get_xxx()の型
ada::url_aggregator parse()内で解析結果を1つのstd::stringで保持して、get_xxx()ではその部分文字列を返す std::string_view
ada::url parse()内で各区分ごとにstd::stringを作成・保持する std::stringだったりstd::string_viewだったり色々

このため test01.cpp の内容だったら ada::url に変更するだけで動作します。

test04.cpp
#include <ada.h>

auto main() -> int {
  auto const url = "https://github.com/ada-url/ada";
  if (auto const result = ada::parse<ada::url>(url)) {
    std::cout << "host : " << result->get_host() << '\n';
  }

  return 0;
}

ada::url使う場面は次のような感じですかね。

  • パースした後の一部分だけ長いスコープで持ちたい
  • URLの一部を更新する (更新処理は後で出てきます)

普通はada::url_aggregatorで良い気がします。

ada::url_aggregatorはada 2.0で追加された機能で、これによりadaのパース性能は20%ぐらい向上したみたいですね。
Announcing Ada URL parser v2.0

togetoge

ada::parse()の戻り値はada::resultとなっていますが、実態はadaにとりこまれたexpectedになっています。

なので、次のようなコードも動きます。
「意味があるか?」と言われるとあんまりないですかね。

test05.cpp
#include <ada.h>

auto main() -> int {
  auto const url = "https://github.com/ada-url/ada";
  ada::parse<ada::url_aggregator>(url)
    .map([](ada::url_aggregator const& result) {
      std::cout << result.get_host() << '\n';
    });

  return 0;
}
togetoge

adaではパース結果に対して変更をすることもできます。

test06.cpp
#include <ada.h>

auto main() -> int {
  auto const url = "https://github.com/ada-url/ada";

  std::cout << "original : " << url << '\n';

  if (auto result = ada::parse<ada::url_aggregator>(url)) {
    result->set_host("bitbucket.org");
    std::cout << "modified : " << result->get_href() << '\n';

    result->set_port("18880");
    std::cout << "modified : " << result->get_href() << '\n';

    result->set_protocol("http");
    std::cout << "modified : " << result->get_href() << '\n';
  }

  return 0;
}

実行すると次のように出力されます。

original : https://github.com/ada-url/ada
modified : https://bitbucket.org/ada-url/ada
modified : https://bitbucket.org:18880/ada-url/ada
modified : http://bitbucket.org:18880/ada-url/ada

私の用途では、URLがパースさえできればいいので、あんまり使わない機能な気がします。

togetoge

日本語ドメインのパースもできました。偉い。

test07.cpp
#include <ada.h>

auto main() -> int {
  auto const url = "https://日本語.jp/case/accessible/all.html";
  std::cout << "original : " << url << '\n';

  if (auto const result = ada::parse<ada::url_aggregator>(url)) {
    std::cout << "hostname : " << result->get_host() << '\n';
  }

  return 0;
}

ちゃんとPunycode変換されているのが分かります。

original : https://日本語.jp/case/accessible/all.html
hostname : xn--wgv71a119e.jp
togetoge

Punycodeの逆変換を勝手にやってくれるわけではありません。(当然か)
これはadaが内部的に利用しているidnaの関数を利用することができます。

test08.cpp
#include <ada.h>

auto main() -> int {
  auto const url = "https://xn--wgv71a119e.jp/case/accessible/all.html";

  std::cout << "original : " << url << '\n';

  if (auto const result = ada::parse<ada::url_aggregator>(url)) {
    std::cout << "hostname : " << result->get_host() << '\n';
    std::cout << "         : " << ada::idna::to_unicode(result->get_host()) << '\n';
  }

  return 0;
}

実行結果です。

original : https://xn--wgv71a119e.jp/case/accessible/all.html
hostname : xn--wgv71a119e.jp
         : 日本語.jp

たぶん、これは非推奨のやり方なので将来的に使えなくなる可能性は高いですね。
素直にidnaをインストールして使いましょう。

togetoge

だいたい興味のある機能は触ってみたので、最後に公式ベンチマークを実行してみて終わりにしようと思います。
公式ベンチマークはCMakeでconfigureする時にADA_BENCHMARKを有効にすることでビルドできるようになります。内部でしれっとrustの開発環境が必要になるので、インストールしていない人は途中でエラーになってしまうかも。

cd /tmp
wget -nd https://github.com/ada-url/ada/archive/refs/tags/v2.3.0.tar.gz -O - | tar zxvf -
cd ada-2.3.0
cmake -B build -S . -DADA_BENCHMARKS=ON
cmake --build build
build/benchmark/bench

軽く Ryzen5 5600X の環境で実行してみた結果がこちら。

parser Time CPU Iterations
BasicBench_AdaURL 3140 ns 3132 ns 220056
BasicBench_AdaURL_aggregator 2017 ns 2013 ns 357261
BasicBench_whatwg 4728 ns 4716 ns 143744
BasicBench_CURL 9752 ns 9733 ns 66976
BasicBench_ServoUrl 7236 ns 7218 ns 92216

なるほど、確かに速い。url_aggregatorもちゃんと優位な差が出てますね。

ベンチマークが何種類かあるので、色々ためすと結構数値は変わりましたが、adaが速いことに変わりなしでした。

togetoge

ひとまずこれで一段落とします。

  • かなり直感的に使える
  • 高速に動く
  • node.jsで採用されているから当分メンテナンスされるだろう安心感
  • 日本語ドメインにも対応

という強みがあり、弱点が見あたらないので徐々にadaに移行しようかなと思っています。

togetoge

Ada 2.4.2がリリースされて、simdjsonやsimdutfのコミッタでもあるDaniel Lemireさん中心に色々チューニングが進んでいるみたいなので、再計測してみました。

parser Time CPU Iterations
BasicBench_AdaURL 3002 ns 2998 ns 228199
BasicBench_AdaURL_aggregator 1906 ns 1903 ns 378945
BasicBench_whatwg 4799 ns 4792 ns 141125
BasicBench_CURL 9805 ns 9792 ns 70939
BasicBench_ServoUrl 7068 ns 7059 ns 84714

Ada urlもAda url_aggregatorも4-5%ぐらい高速化してますね。
今後の高速化にも期待できそうです。