Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/performance #1002

Merged
merged 8 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@

Cargo.lock
.DS_Store

# ベンチマーク結果
/benchmark_results
1 change: 1 addition & 0 deletions parser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ workflow = "Rust"
[dependencies]
log = "0.4.21"
regex = "1.10.4"
fnv = "1.0.7"

[dev-dependencies]
anyhow = "1.0.82"
Expand Down
16 changes: 2 additions & 14 deletions parser/benches/bench_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,10 @@

use criterion::*;

use crate::nom_json::nom_parse_json;
use crate::oni_comb_json::oni_comb_parse_json;
use crate::pom_json::pom_parse_json;
// use pprof::criterion::{Output, PProfProfiler};

mod nom_json;
mod oni_comb_json;
mod pom_json;

/// 異なる複雑さのJSONデータを用意
fn get_test_data() -> Vec<(&'static str, &'static str)> {
Expand Down Expand Up @@ -64,22 +60,14 @@ fn get_test_data() -> Vec<(&'static str, &'static str)> {

fn criterion_benchmark(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("json");

// 各テストデータに対してベンチマークを実行
for (name, data) in get_test_data() {
group.bench_with_input(BenchmarkId::new("oni-comb-rs", name), data, |b, i| {
b.iter(|| oni_comb_parse_json(i))
});

group.bench_with_input(BenchmarkId::new("nom", name), data, |b, i| {
b.iter(|| nom_parse_json(i))
});

group.bench_with_input(BenchmarkId::new("pom", name), data, |b, i| {
b.iter(|| pom_parse_json(i))
});
}

group.finish();
}

Expand Down
182 changes: 156 additions & 26 deletions parser/benches/oni_comb_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,63 +36,63 @@ fn string<'a>() -> Parser<'a, char, String> {
| elm_ref('n').map(|_| &'\n')
| elm_ref('r').map(|_| &'\r')
| elm_ref('t').map(|_| &'\t');

// エスケープシーケンス
let escape_sequence = elm_ref('\\') * special_char;

// 通常の文字列(エスケープシーケンスを含む)
let char_string = (none_ref_of("\\\"") | escape_sequence)
.map(|c| *c) // Clone::clone の代わりに参照外し
.of_many1()
.map(String::from_iter);

// UTF-16文字の処理(ベンチマークでは使用されないが、完全性のために残す)
let utf16_char: Parser<char, u16> = tag("\\u")
* elm_pred(|c: &char| c.is_digit(16))
.of_count(4)
.map(String::from_iter)
.map_res(|digits| u16::from_str_radix(&digits, 16));

let utf16_string = utf16_char.of_many1().map(|chars| {
decode_utf16(chars)
.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
.collect::<String>()
});

// 文字列全体
let string = surround(
elm_ref('"'),
(char_string | utf16_string).of_many0().cache(), // キャッシュを追加
elm_ref('"')
elm_ref('"'),
);

string.map(|strings| strings.concat())
}

fn array<'a>() -> Parser<'a, char, Vec<JsonValue>> {
// 空白を含むカンマ区切りのパターン
let comma_sep = space() * elm_ref(',') - space();

// 配列要素のパーサー(遅延評価)
let elems = lazy(value).cache().of_many0_sep(comma_sep);

// 配列全体のパーサー(角括弧で囲まれた要素)
surround(elm_ref('[') - space(), elems, space() * elm_ref(']'))
}

fn object<'a>() -> Parser<'a, char, HashMap<String, JsonValue>> {
// キーと値のペアのパーサー
let member = string().cache() - space() - elm_ref(':') - space() + lazy(value).cache();

// 空白を含むカンマ区切りのパターン
let comma_sep = space() * elm_ref(',') - space();

// オブジェクトメンバーのパーサー
let members = member.of_many0_sep(comma_sep);

// オブジェクト全体のパーサー(波括弧で囲まれたメンバー)
let obj = surround(elm_ref('{') - space(), members, space() * elm_ref('}'));

// メンバーをHashMapに変換
obj.map(|members| members.into_iter().collect::<HashMap<_, _>>())
}
Expand All @@ -104,17 +104,13 @@ fn boolean<'a>() -> Parser<'a, char, bool> {
fn value<'a>() -> Parser<'a, char, JsonValue> {
// 各種JSONの値をパースするパーサーを組み合わせる
// 最も頻度の高いものから順に試す(パフォーマンス向上のため)
(
// 単純な値(頻度が高い順)
string().map(|text| JsonValue::Str(text)).cache() |
number().map(|num| JsonValue::Num(num)).cache() |
boolean().map(|b| JsonValue::Bool(b)).cache() |
tag("null").map(|_| JsonValue::Null).cache() |

// 複合型(再帰的なパーサー)
array().map(|arr| JsonValue::Array(arr)).cache() |
object().map(|obj| JsonValue::Object(obj)).cache()
) - space()
(string().map(|text| JsonValue::Str(text)).cache()
| number().map(|num| JsonValue::Num(num)).cache()
| boolean().map(|b| JsonValue::Bool(b)).cache()
| tag("null").map(|_| JsonValue::Null).cache()
| array().map(|arr| JsonValue::Array(arr)).cache()
| object().map(|obj| JsonValue::Object(obj)).cache())
- space()
}

pub fn json<'a>() -> Parser<'a, char, JsonValue> {
Expand All @@ -125,10 +121,144 @@ pub fn json<'a>() -> Parser<'a, char, JsonValue> {
pub fn oni_comb_parse_json(s: &str) {
// 文字列を文字のベクターに変換
let input: Vec<char> = s.chars().collect();

// キャッシュを有効にしたパーサーを使用
let parser = json();

// パース実行
let _ = parser.parse(&input).success().unwrap();
}

// バイトレベルでのJSONパーサー
// 注: 現在は使用していませんが、将来的にはこちらに移行する予定
mod byte_json {
use oni_comb_parser_rs::prelude::*;
use std::collections::HashMap;
use std::str::FromStr;

#[derive(Clone, Debug, PartialEq)]
pub enum JsonValue {
Null,
Bool(bool),
Str(String),
Num(f64),
Array(Vec<JsonValue>),
Object(HashMap<String, JsonValue>),
}

// 空白文字をスキップ
pub fn space<'a>() -> Parser<'a, u8, ()> {
elm_of(&b" \t\r\n"[..]).of_many0().discard()
}

// 数値のパース
pub fn number<'a>() -> Parser<'a, u8, f64> {
// 整数部分
let integer =
elm_pred(|b: &u8| *b >= b'1' && *b <= b'9') - elm_pred(|b: &u8| *b >= b'0' && *b <= b'9').of_many0() | elm(b'0');

// 小数部分
let frac = elm(b'.') + elm_pred(|b: &u8| *b >= b'0' && *b <= b'9').of_many1();

// 指数部分
let exp = elm_of(&b"eE"[..]) + elm_of(&b"+-"[..]).opt() + elm_pred(|b: &u8| *b >= b'0' && *b <= b'9').of_many1();

// 数値全体
let number = elm(b'-').opt() + integer + frac.opt() + exp.opt();

// バイト列を文字列に変換し、さらに浮動小数点数に変換
number.collect().map(|bytes| {
let s = std::str::from_utf8(bytes).unwrap();
f64::from_str(s).unwrap()
})
}

// 文字列のパース
pub fn string<'a>() -> Parser<'a, u8, String> {
// エスケープシーケンスの処理
let escape_char = elm_ref(b'\\')
* (elm(b'\\')
| elm(b'/')
| elm(b'"')
| elm(b'b').map(|_| b'\x08')
| elm(b'f').map(|_| b'\x0C')
| elm(b'n').map(|_| b'\n')
| elm(b'r').map(|_| b'\r')
| elm(b't').map(|_| b'\t'));

// 通常の文字(エスケープや引用符以外)
let regular_char = elm_pred(|b: &u8| *b != b'\\' && *b != b'"');

// 文字列内の任意の文字
let string_char = regular_char | escape_char;

// 文字列全体
let string_parser = elm(b'"') * string_char.of_many0().collect().cache() - elm(b'"');

// バイト列を文字列に変換
string_parser.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
}

// 真偽値のパース
pub fn boolean<'a>() -> Parser<'a, u8, bool> {
seq(b"true").map(|_| true) | seq(b"false").map(|_| false)
}

// 配列のパース
pub fn array<'a>() -> Parser<'a, u8, Vec<JsonValue>> {
// 空白を含むカンマ区切りのパターン
let comma_sep = space() * elm(b',') - space();

// 配列要素のパーサー(遅延評価)
let elems = lazy(value).cache().of_many0_sep(comma_sep);

// 配列全体のパーサー(角括弧で囲まれた要素)
surround(elm(b'[') - space(), elems, space() * elm(b']'))
}

// オブジェクトのパース
pub fn object<'a>() -> Parser<'a, u8, HashMap<String, JsonValue>> {
// キーと値のペアのパーサー
let member = string().cache() - space() - elm(b':') - space() + lazy(value).cache();

// 空白を含むカンマ区切りのパターン
let comma_sep = space() * elm(b',') - space();

// オブジェクトメンバーのパーサー
let members = member.of_many0_sep(comma_sep);

// オブジェクト全体のパーサー(波括弧で囲まれたメンバー)
let obj = surround(elm(b'{') - space(), members, space() * elm(b'}'));

// メンバーをHashMapに変換
obj.map(|members| members.into_iter().collect::<HashMap<_, _>>())
}

// JSON値のパース
pub fn value<'a>() -> Parser<'a, u8, JsonValue> {
// 各種JSONの値をパースするパーサーを組み合わせる
// 最も頻度の高いものから順に試す(パフォーマンス向上のため)
(
// 単純な値(頻度が高い順)
string().map(|text| JsonValue::Str(text)).cache()
| number().map(|num| JsonValue::Num(num)).cache()
| boolean().map(|b| JsonValue::Bool(b)).cache()
| seq(b"null").map(|_| JsonValue::Null).cache()
| array().map(|arr| JsonValue::Array(arr)).cache()
| object().map(|obj| JsonValue::Object(obj)).cache()
) - space()
}

// JSONドキュメント全体のパース
pub fn json<'a>() -> Parser<'a, u8, JsonValue> {
// 先頭の空白をスキップし、値をパースし、終端を確認
space() * value().cache() - end()
}

// ベンチマーク用の関数
pub fn parse_json(s: &str) -> JsonValue {
// 文字列をバイト列として直接処理
let input = s.as_bytes();
json().parse(input).success().unwrap()
}
}
21 changes: 15 additions & 6 deletions parser/src/internal/parsers_impl/cache_parsers_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,37 @@ use crate::extension::parsers::CacheParsers;
use crate::internal::ParsersImpl;
use std::cell::RefCell;

use std::collections::HashMap;
use fnv::FnvHashMap;
use std::fmt::Debug;
use std::ptr;

impl CacheParsers for ParsersImpl {
fn cache<'a, I, A>(parser: Self::P<'a, I, A>) -> Self::P<'a, I, A>
where
I: Clone + 'a,
A: Clone + Debug + 'a, {
let caches = RefCell::new(HashMap::new());
// FnvHashMapを使用してキャッシュを作成(キーは単純な文字列ではなくタプル)
let caches = RefCell::new(FnvHashMap::<(usize, usize, usize), ParseResult<'a, I, A>>::default());

Parser::new(move |parser_state| {
let key = format!(
"{:p}:{}:{:p}",
parser_state,
// キーをタプルとして生成(文字列変換なし)
let key = (
parser_state as *const _ as usize,
parser_state.last_offset().unwrap_or(0),
&parser.method
ptr::addr_of!(parser.method) as usize,
);

// キャッシュから結果を取得または計算
let parse_result = caches
.borrow_mut()
.entry(key)
.or_insert_with(|| parser.run(parser_state))
.clone();

parse_result
})
}
}

// ParseResultの型をインポート
use crate::core::ParseResult;
5 changes: 5 additions & 0 deletions run-benchmark.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#!/usr/bin/env sh

set -e

export CLICOLOR_FORCE=1
export FORCE_COLOR=1

# ベンチマークの実行と結果の保存
echo "JSONパーサーのベンチマークを実行します..."

Expand Down
Loading