mirror of
https://github.com/fastfloat/fast_float.git
synced 2026-07-30 16:26:21 +08:00
Make digit separator / prefix support performance-neutral
Re-implements the optional digit-separator and base-prefix parsing (originally PR #369) on top of the current store_spans hot-path architecture, with the goal of zero overhead when the features are not used. Key changes vs. the original PR: - has_separator is now a *compile-time* template parameter on parse_number_string, dispatched once (from_chars_advanced -> parse_number_string_options) based on options.digit_separator. The has_separator==false instantiation that every default caller uses is byte-for-byte the separator-free parser: no separator comparison ever enters a digit loop and the SIMD eight-digit fast path is preserved. The separator-aware code lives only in the cold true instantiation. - The store_spans no-span hot path (added to main after the original PR was branched) is preserved. - parse_options_t fields are ordered so the two new single-byte fields (digit_separator, format_options) fall into the existing padding for UC == char. sizeof(parse_options_t<char>) stays 16 bytes, so the struct is still register-passed and the call boundary is unchanged. Result: for the default (no separator, no prefix) path, the generated assembly of from_chars<double> is identical to main, and benchmarks show no measurable regression (the original PR was 5-10% slower). Adds basictest coverage for digit separators (including the >19-digit overflow re-scan paths) and prefix skipping.
This commit is contained in:
parent
34164f547b
commit
1d35d26b48
@ -337,12 +337,24 @@ report_parse_error(UC const *p, parse_error error) {
|
||||
// spans (read only by the rare digit_comp slow path) are not materialized,
|
||||
// which keeps the fat parsed_number_string_t off the hot path. The caller
|
||||
// re-parses with store_spans=true if the slow path is actually reached.
|
||||
template <bool basic_json_fmt, typename UC>
|
||||
//
|
||||
// has_separator is a *compile-time* flag (the opposite choice from store_spans,
|
||||
// and deliberately so): the separator-aware code paths are an opt-in feature
|
||||
// that the vast majority of callers never enable. Gating them on a template
|
||||
// parameter means the has_separator==false instantiation -- the default that
|
||||
// everybody uses -- compiles to exactly the same code as if the feature did not
|
||||
// exist: no separator comparison ever enters a digit loop, and the SIMD
|
||||
// eight-digit fast path stays intact. The has_separator==true instantiation is
|
||||
// cold code that default callers never execute. See parse_number_string_options
|
||||
// for the runtime->compile-time dispatch.
|
||||
template <bool basic_json_fmt, bool has_separator, typename UC>
|
||||
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t<UC>
|
||||
parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
|
||||
bool store_spans = true) noexcept {
|
||||
chars_format const fmt = detail::adjust_for_feature_macros(options.format);
|
||||
UC const decimal_point = options.decimal_point;
|
||||
UC const separator = options.digit_separator;
|
||||
(void)separator; // unused when has_separator == false
|
||||
|
||||
parsed_number_string_t<UC> answer;
|
||||
answer.valid = false;
|
||||
@ -375,16 +387,19 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
|
||||
UC const *const start_digits = p;
|
||||
|
||||
uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad)
|
||||
int64_t digit_count = 0;
|
||||
// Points at the first actual digit (== start_digits when no separator
|
||||
// precedes it). Used only by the basic_json leading-zero check.
|
||||
UC const *first_digit_ptr = start_digits;
|
||||
(void)first_digit_ptr; // only read in the basic_json_fmt path
|
||||
|
||||
// Straight-line unroll of the integer-part scan: most integer parts are
|
||||
// 1-5 digits, so peeling the first iterations eliminates the loop back-edge
|
||||
// for the common case. Semantics are identical to the original `while` loop:
|
||||
// i = 10*i + digit, advancing p.
|
||||
if ((p != pend) && is_integer(*p)) {
|
||||
i = uint64_t(*p - UC('0'));
|
||||
++p;
|
||||
FASTFLOAT_IF_CONSTEXPR17(!has_separator) {
|
||||
// Straight-line unroll of the integer-part scan: most integer parts are
|
||||
// 1-5 digits, so peeling the first iterations eliminates the loop back-edge
|
||||
// for the common case. Semantics are identical to the original `while`
|
||||
// loop: i = 10*i + digit, advancing p.
|
||||
if ((p != pend) && is_integer(*p)) {
|
||||
i = 10 * i + uint64_t(*p - UC('0'));
|
||||
i = uint64_t(*p - UC('0'));
|
||||
++p;
|
||||
if ((p != pend) && is_integer(*p)) {
|
||||
i = 10 * i + uint64_t(*p - UC('0'));
|
||||
@ -395,29 +410,58 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
|
||||
if ((p != pend) && is_integer(*p)) {
|
||||
i = 10 * i + uint64_t(*p - UC('0'));
|
||||
++p;
|
||||
while ((p != pend) && is_integer(*p)) {
|
||||
// a multiplication by 10 is cheaper than an arbitrary integer
|
||||
// multiplication
|
||||
i = 10 * i +
|
||||
uint64_t(*p - UC('0')); // might overflow, handled later
|
||||
if ((p != pend) && is_integer(*p)) {
|
||||
i = 10 * i + uint64_t(*p - UC('0'));
|
||||
++p;
|
||||
while ((p != pend) && is_integer(*p)) {
|
||||
// a multiplication by 10 is cheaper than an arbitrary integer
|
||||
// multiplication
|
||||
i = 10 * i +
|
||||
uint64_t(*p - UC('0')); // might overflow, handled later
|
||||
++p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
digit_count = int64_t(p - start_digits);
|
||||
}
|
||||
else {
|
||||
// Separator-aware scan: a configured digit separator (e.g. '\'') may appear
|
||||
// between digits. It is skipped and does not contribute to the value or the
|
||||
// digit count, but it is retained in the integer span below so the overflow
|
||||
// re-scan can re-tokenize correctly.
|
||||
while (p != pend) {
|
||||
if (*p == separator) {
|
||||
++p;
|
||||
continue;
|
||||
}
|
||||
if (!is_integer(*p)) {
|
||||
break;
|
||||
}
|
||||
if (digit_count == 0) {
|
||||
first_digit_ptr = p;
|
||||
}
|
||||
i = 10 * i + uint64_t(*p - UC('0')); // might overflow, handled later
|
||||
++p;
|
||||
++digit_count;
|
||||
}
|
||||
}
|
||||
UC const *const end_of_integer_part = p;
|
||||
int64_t digit_count = int64_t(end_of_integer_part - start_digits);
|
||||
if (store_spans) {
|
||||
answer.integer = span<UC const>(start_digits, size_t(digit_count));
|
||||
// The span keeps the raw characters (separators included) so the overflow
|
||||
// re-scan below can re-tokenize correctly; for has_separator == false the
|
||||
// length equals digit_count.
|
||||
answer.integer = span<UC const>(start_digits,
|
||||
size_t(end_of_integer_part - start_digits));
|
||||
}
|
||||
FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) {
|
||||
// at least 1 digit in integer part, without leading zeros
|
||||
if (digit_count == 0) {
|
||||
return report_parse_error<UC>(p, parse_error::no_digits_in_integer_part);
|
||||
}
|
||||
if ((start_digits[0] == UC('0') && digit_count > 1)) {
|
||||
if ((*first_digit_ptr == UC('0') && digit_count > 1)) {
|
||||
return report_parse_error<UC>(start_digits,
|
||||
parse_error::leading_zeros_in_integer_part);
|
||||
}
|
||||
@ -428,20 +472,40 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
|
||||
if (has_decimal_point) {
|
||||
++p;
|
||||
UC const *before = p;
|
||||
// can occur at most twice without overflowing, but let it occur more, since
|
||||
// for integers with many digits, digit parsing is the primary bottleneck.
|
||||
loop_parse_if_eight_digits(p, pend, i);
|
||||
int64_t fractional_digit_count = 0;
|
||||
FASTFLOAT_IF_CONSTEXPR17(!has_separator) {
|
||||
// can occur at most twice without overflowing, but let it occur more,
|
||||
// since for integers with many digits, digit parsing is the primary
|
||||
// bottleneck.
|
||||
loop_parse_if_eight_digits(p, pend, i);
|
||||
|
||||
while ((p != pend) && is_integer(*p)) {
|
||||
uint8_t digit = uint8_t(*p - UC('0'));
|
||||
++p;
|
||||
i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
|
||||
while ((p != pend) && is_integer(*p)) {
|
||||
uint8_t digit = uint8_t(*p - UC('0'));
|
||||
++p;
|
||||
i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
|
||||
}
|
||||
fractional_digit_count = int64_t(p - before);
|
||||
}
|
||||
exponent = before - p;
|
||||
else {
|
||||
while (p != pend) {
|
||||
if (*p == separator) {
|
||||
++p;
|
||||
continue;
|
||||
}
|
||||
if (!is_integer(*p)) {
|
||||
break;
|
||||
}
|
||||
uint8_t digit = uint8_t(*p - UC('0'));
|
||||
++p;
|
||||
i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
|
||||
++fractional_digit_count;
|
||||
}
|
||||
}
|
||||
exponent = -fractional_digit_count;
|
||||
if (store_spans) {
|
||||
answer.fraction = span<UC const>(before, size_t(p - before));
|
||||
}
|
||||
digit_count -= exponent;
|
||||
digit_count += fractional_digit_count;
|
||||
}
|
||||
FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) {
|
||||
// at least 1 digit in fractional part
|
||||
@ -483,12 +547,30 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
|
||||
// Otherwise, we will be ignoring the 'e'.
|
||||
p = location_of_e;
|
||||
} else {
|
||||
while ((p != pend) && is_integer(*p)) {
|
||||
uint8_t digit = uint8_t(*p - UC('0'));
|
||||
if (exp_number < 0x10000000) {
|
||||
exp_number = 10 * exp_number + digit;
|
||||
FASTFLOAT_IF_CONSTEXPR17(!has_separator) {
|
||||
while ((p != pend) && is_integer(*p)) {
|
||||
uint8_t digit = uint8_t(*p - UC('0'));
|
||||
if (exp_number < 0x10000000) {
|
||||
exp_number = 10 * exp_number + digit;
|
||||
}
|
||||
++p;
|
||||
}
|
||||
}
|
||||
else {
|
||||
while (p != pend) {
|
||||
if (*p == separator) {
|
||||
++p;
|
||||
continue;
|
||||
}
|
||||
if (!is_integer(*p)) {
|
||||
break;
|
||||
}
|
||||
uint8_t digit = uint8_t(*p - UC('0'));
|
||||
if (exp_number < 0x10000000) {
|
||||
exp_number = 10 * exp_number + digit;
|
||||
}
|
||||
++p;
|
||||
}
|
||||
++p;
|
||||
}
|
||||
if (neg_exp) {
|
||||
exp_number = -exp_number;
|
||||
@ -514,9 +596,12 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
|
||||
// It is possible that the integer had an overflow.
|
||||
// We have to handle the case where we have 0.0000somenumber.
|
||||
// We need to be mindful of the case where we only have zeroes...
|
||||
// E.g., 0.000000000...000.
|
||||
// E.g., 0.000000000...000. The `has_separator &&` guard below is a
|
||||
// compile-time constant, so this loop is identical to the original when the
|
||||
// feature is disabled.
|
||||
UC const *start = start_digits;
|
||||
while ((start != pend) && (*start == UC('0') || *start == decimal_point)) {
|
||||
while ((start != pend) && (*start == UC('0') || *start == decimal_point ||
|
||||
(has_separator && *start == separator))) {
|
||||
if (*start == UC('0')) {
|
||||
digit_count--;
|
||||
}
|
||||
@ -537,20 +622,60 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
|
||||
p = answer.integer.ptr;
|
||||
UC const *int_end = p + answer.integer.len();
|
||||
uint64_t const minimal_nineteen_digit_integer{1000000000000000000};
|
||||
while ((i < minimal_nineteen_digit_integer) && (p != int_end)) {
|
||||
i = i * 10 + uint64_t(*p - UC('0'));
|
||||
++p;
|
||||
}
|
||||
if (i >= minimal_nineteen_digit_integer) { // We have a big integer
|
||||
exponent = end_of_integer_part - p + exp_number;
|
||||
} else { // We have a value with a fractional component.
|
||||
p = answer.fraction.ptr;
|
||||
UC const *frac_end = p + answer.fraction.len();
|
||||
while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
|
||||
FASTFLOAT_IF_CONSTEXPR17(!has_separator) {
|
||||
while ((i < minimal_nineteen_digit_integer) && (p != int_end)) {
|
||||
i = i * 10 + uint64_t(*p - UC('0'));
|
||||
++p;
|
||||
}
|
||||
exponent = answer.fraction.ptr - p + exp_number;
|
||||
if (i >= minimal_nineteen_digit_integer) { // We have a big integer
|
||||
exponent = end_of_integer_part - p + exp_number;
|
||||
} else { // We have a value with a fractional component.
|
||||
p = answer.fraction.ptr;
|
||||
UC const *frac_end = p + answer.fraction.len();
|
||||
while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
|
||||
i = i * 10 + uint64_t(*p - UC('0'));
|
||||
++p;
|
||||
}
|
||||
exponent = answer.fraction.ptr - p + exp_number;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Separator-aware re-scan: separators are skipped and excluded from
|
||||
// the digit counts that determine the exponent.
|
||||
while ((i < minimal_nineteen_digit_integer) && (p != int_end)) {
|
||||
if (*p == separator) {
|
||||
++p;
|
||||
continue;
|
||||
}
|
||||
i = i * 10 + uint64_t(*p - UC('0'));
|
||||
++p;
|
||||
}
|
||||
if (i >= minimal_nineteen_digit_integer) { // We have a big integer
|
||||
int64_t remaining_integer_digits = 0;
|
||||
while (p != int_end) {
|
||||
if (*p == separator) {
|
||||
++p;
|
||||
continue;
|
||||
}
|
||||
++p;
|
||||
++remaining_integer_digits;
|
||||
}
|
||||
exponent = remaining_integer_digits + exp_number;
|
||||
} else { // We have a value with a fractional component.
|
||||
p = answer.fraction.ptr;
|
||||
UC const *frac_end = p + answer.fraction.len();
|
||||
int64_t fraction_digits_consumed = 0;
|
||||
while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
|
||||
if (*p == separator) {
|
||||
++p;
|
||||
continue;
|
||||
}
|
||||
i = i * 10 + uint64_t(*p - UC('0'));
|
||||
++p;
|
||||
++fraction_digits_consumed;
|
||||
}
|
||||
exponent = exp_number - fraction_digits_consumed;
|
||||
}
|
||||
}
|
||||
// We have now corrected both exponent and i, to a truncated value
|
||||
}
|
||||
|
||||
@ -70,15 +70,35 @@ using from_chars_result = from_chars_result_t<char>;
|
||||
|
||||
template <typename UC> struct parse_options_t {
|
||||
constexpr explicit parse_options_t(chars_format fmt = chars_format::general,
|
||||
UC dot = UC('.'), int b = 10)
|
||||
: format(fmt), decimal_point(dot), base(b) {}
|
||||
UC dot = UC('.'), int b = 10,
|
||||
UC sep = UC('\0'), uint8_t opts = 0)
|
||||
: format(fmt), decimal_point(dot), digit_separator(sep),
|
||||
format_options(opts), base(b) {}
|
||||
|
||||
// Member order is chosen so that, for the common UC == char case, the two
|
||||
// new single-byte fields land in the padding that already existed between
|
||||
// decimal_point and base. This keeps sizeof(parse_options_t<char>) == 16, so
|
||||
// the struct is still passed in registers (ARM64/x86-64) and the default
|
||||
// parse path is unaffected. Reordering would grow the struct and force it
|
||||
// onto the stack at the call boundary.
|
||||
|
||||
/** Which number formats are accepted */
|
||||
chars_format format;
|
||||
/** The character used as decimal point */
|
||||
UC decimal_point;
|
||||
/** The character used as digit separator (e.g. '\''). Use '\0' to disable.
|
||||
* When disabled (the default), the parser compiles to the exact same code as
|
||||
* if this option did not exist: separator handling is gated on a compile-time
|
||||
* template parameter, so the default hot path carries no extra branches. */
|
||||
UC digit_separator;
|
||||
/** Additional format options (bitmask), see the static flags below. */
|
||||
uint8_t format_options;
|
||||
/** The base used for integers */
|
||||
int base;
|
||||
|
||||
/** Skip a leading base prefix (0x/0X, 0b/0B) before parsing. Decimal-only:
|
||||
* the digits are still parsed in base 10, the prefix is merely consumed. */
|
||||
static constexpr uint8_t skip_prefix = 1;
|
||||
};
|
||||
|
||||
using parse_options = parse_options_t<char>;
|
||||
|
||||
@ -289,6 +289,30 @@ from_chars_advanced(parsed_number_string_t<UC> &pns, T &value) noexcept {
|
||||
return answer;
|
||||
}
|
||||
|
||||
// Runtime -> compile-time dispatch over both boolean knobs of
|
||||
// parse_number_string. basic_json_fmt was already dispatched this way; the
|
||||
// digit separator is selected here too so that the separator-aware code paths
|
||||
// stay confined to the (cold) has_separator==true instantiation. Callers that
|
||||
// never set a separator -- the overwhelming majority -- run the
|
||||
// has_separator==false instantiation, which is byte-for-byte the original
|
||||
// separator-free parser.
|
||||
template <typename UC>
|
||||
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t<UC>
|
||||
parse_number_string_options(UC const *first, UC const *last,
|
||||
parse_options_t<UC> options, bool bjf,
|
||||
bool store_spans) noexcept {
|
||||
if (options.digit_separator != UC('\0')) {
|
||||
return bjf ? parse_number_string<true, true, UC>(first, last, options,
|
||||
store_spans)
|
||||
: parse_number_string<false, true, UC>(first, last, options,
|
||||
store_spans);
|
||||
}
|
||||
return bjf ? parse_number_string<true, false, UC>(first, last, options,
|
||||
store_spans)
|
||||
: parse_number_string<false, false, UC>(first, last, options,
|
||||
store_spans);
|
||||
}
|
||||
|
||||
// Slow path: re-parse materializing the integer/fraction spans the hot no-span
|
||||
// parse skipped, then run the full algorithm. The two callers reach it only
|
||||
// through a fastfloat_unlikely branch, so the optimizer keeps this re-parse off
|
||||
@ -301,8 +325,7 @@ FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
|
||||
parse_number_slow_path(UC const *first, UC const *last, T &value,
|
||||
parse_options_t<UC> options, bool bjf) noexcept {
|
||||
parsed_number_string_t<UC> pns =
|
||||
bjf ? parse_number_string<true, UC>(first, last, options, true)
|
||||
: parse_number_string<false, UC>(first, last, options, true);
|
||||
parse_number_string_options(first, last, options, bjf, true);
|
||||
return from_chars_advanced(pns, value);
|
||||
}
|
||||
|
||||
@ -336,8 +359,7 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value,
|
||||
// parsed_number_string_t off the hot path. store_spans is a runtime argument,
|
||||
// so this reuses the single parse_number_string instantiation.
|
||||
parsed_number_string_t<UC> pns =
|
||||
bjf ? parse_number_string<true, UC>(first, last, options, false)
|
||||
: parse_number_string<false, UC>(first, last, options, false);
|
||||
parse_number_string_options(first, last, options, bjf, false);
|
||||
if (!pns.valid) {
|
||||
if (uint64_t(fmt & chars_format::no_infnan)) {
|
||||
answer.ec = std::errc::invalid_argument;
|
||||
@ -539,6 +561,13 @@ template <typename T, typename UC>
|
||||
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
|
||||
from_chars_advanced(UC const *first, UC const *last, T &value,
|
||||
parse_options_t<UC> options) noexcept {
|
||||
if (((options.format_options & parse_options_t<UC>::skip_prefix) != 0) &&
|
||||
(last - first >= 2) && (*first == UC('0'))) {
|
||||
UC const c_low = UC(first[1] | UC(0x20));
|
||||
if (c_low == UC('x') || c_low == UC('b')) {
|
||||
first += 2;
|
||||
}
|
||||
}
|
||||
return from_chars_advanced_caller<
|
||||
size_t(is_supported_float_type<T>::value) +
|
||||
2 * size_t(is_supported_integer_type<T>::value)>::call(first, last, value,
|
||||
|
||||
@ -681,6 +681,47 @@ TEST_CASE("decimal_point_parsing") {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("digit_separator") {
|
||||
double result;
|
||||
fast_float::parse_options options{};
|
||||
options.digit_separator = '_';
|
||||
auto parse = [&](std::string const &input, double expected) {
|
||||
auto answer = fast_float::from_chars_advanced(
|
||||
input.data(), input.data() + input.size(), result, options);
|
||||
CHECK_MESSAGE(answer.ec == std::errc(), "expected parse success");
|
||||
CHECK_MESSAGE(answer.ptr == input.data() + input.size(),
|
||||
"Parsing should have stopped at end");
|
||||
CHECK_EQ(result, expected);
|
||||
};
|
||||
parse("1_000", 1000.0);
|
||||
parse("1.00_5", 1.005);
|
||||
parse("1e1_0", 1e10);
|
||||
parse("1_5e1_2", 15e12);
|
||||
parse("1_5.0_5e1_2", 15.05e12);
|
||||
// overflow re-scan paths (> 19 significant digits) with separators
|
||||
parse("1_000_000_000_000_000_000_000", 1e21);
|
||||
parse("123_456_789_012_345_678_901_234.5", 123456789012345678901234.5);
|
||||
parse("0.0000000000000000000_1234567890123456789", 1.234567890123456789e-20);
|
||||
}
|
||||
|
||||
TEST_CASE("skip_prefix") {
|
||||
double result;
|
||||
fast_float::parse_options options{};
|
||||
options.format_options = fast_float::parse_options::skip_prefix;
|
||||
auto parse = [&](std::string const &input, double expected) {
|
||||
auto answer = fast_float::from_chars_advanced(
|
||||
input.data(), input.data() + input.size(), result, options);
|
||||
CHECK_MESSAGE(answer.ec == std::errc(), "expected parse success");
|
||||
CHECK_EQ(result, expected);
|
||||
};
|
||||
// prefix is consumed, the remaining digits are parsed in base 10
|
||||
parse("0x10", 10.0);
|
||||
parse("0X25", 25.0);
|
||||
parse("0b11", 11.0);
|
||||
parse("0B11", 11.0);
|
||||
parse("42", 42.0); // no prefix present
|
||||
}
|
||||
|
||||
TEST_CASE("issue19") {
|
||||
std::string const input = "234532.3426362,7869234.9823,324562.645";
|
||||
double result;
|
||||
|
||||
@ -131,7 +131,7 @@ int main() {
|
||||
for (std::size_t i = 0; i < reject.size(); ++i) {
|
||||
auto const &f = reject[i].input;
|
||||
auto const &expected_reason = reject[i].reason;
|
||||
auto answer = fast_float::parse_number_string<true>(
|
||||
auto answer = fast_float::parse_number_string<true, false>(
|
||||
f.data(), f.data() + f.size(),
|
||||
fast_float::parse_options(
|
||||
fast_float::chars_format::json |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user