This is an automated email from the ASF dual-hosted git repository.
mrhhsg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new b2f23b2c7cc [improvement](be) Optimize pow squares with exact integer
results (#68258)
b2f23b2c7cc is described below
commit b2f23b2c7cc70b9f2c1c55baa2267aa99a6e96a2
Author: Jerry Hu <[email protected]>
AuthorDate: Wed Sep 23 17:23:37 2026 +0800
[improvement](be) Optimize pow squares with exact integer results (#68258)
### What problem does this PR solve?
Issue Number: None
Problem Summary:
Speed up POW(double_column, 2.0) for small integral bases without
replacing
the existing libm result for general floating-point inputs.
For round-to-nearest and integer |x| <= 2^26, x*x is an exactly
representable
binary64 integer (at most 2^52). Dispatch once per block for constant
exponent
2, find the safe integer prefix, and use a multiplication loop for that
prefix. The suffix stays on libm; stopping the eligibility scan at the
first
unsafe value avoids per-row checks throughout fractional or mixed
blocks.
The POWER/DPOW/FPOW aliases share the same path.
The original broad specialization was unsafe: pow(x, 2) and x*x can
differ
by one ULP. The tiny fractional counterexample 1.1500729535343723e-17
now
remains on libm, as does the large integral counterexample 94906297,
whose
square is not exactly representable. The fallback reads a volatile
exponent
so optimized compilation cannot silently turn it back into
multiplication.
Directed rounding modes use the original implementation for the whole
block.
Other exponents, FE folding and other physical column shapes are
unchanged.
No execution-version switch or new numeric semantics is introduced.
#### RELEASE SQL performance
SELECT SUM(POW(CAST(number AS DOUBLE), 2.0))
FROM numbers("number" = "20000000");
Local shared host, pipeline parallelism 1, SQL cache disabled, 2 warmups
and
7 measured runs per case, with case order interleaved:
| Rows | Baseline POW | Patched POW | Lower latency | Patched
multiplication |
|---:|---:|---:|---:|---:|
| 2,000,000 | 0.073392 s | 0.020034 s | 72.70% | 0.020318 s |
| 20,000,000 | 0.560169 s | 0.080025 s | 85.71% | 0.075739 s |
At 20M rows this is approximately 7.00x faster and close to direct
multiplication. The unchanged exponent-3 control is 0.561324 -> 0.552684
s;
fractional-input POW is 0.561809 -> 0.552041 s; mixed-input POW is
0.644337 -> 0.634809 s. These roughly 1-2% control variations are
consistent
with shared-host noise, rather than a claimed control-path improvement.
All integer POW sums match between baseline and patched runs.
This improvement is intentionally limited to the safe integer prefix,
not
all DOUBLE inputs or all POW workloads.
### Release note
Improve POW/POWER/DPOW/FPOW with constant exponent 2 for blocks
beginning
with integer bases in [-67108864, 67108864], preserving existing numeric
results and libm fallback behavior.
### Check List (For Author)
- Test:
- RELEASE baseline and patched BE builds; patched ASAN BE build via
build.sh.
- ./run-be-ut.sh --run --filter='MathFunctionTest.*' -j24:
52 passed, 1 existing release-only random_test skipped.
- Exact-bit runtime libm oracle across all aliases, const masks and
nullability modes; safe/unsafe boundaries, fractional and large-integer
one-ULP counterexamples, random bit patterns, empty blocks and all four
rounding modes.
- Exhaustive local SQL parity for every integer magnitude from 0 through
67108864 with both signs, compared against the unchanged vector-exponent
path. Alternating exponents 2/3 prevent a physically constant exponent.
All square cases match; only the expected cube controls differ.
- function_p0/test_pow_square and test_math_function on isolated RELEASE
and ASAN clusters. Golden output generated by -forceGenOut, then checked
without it; all 8 pre-optimization baseline result sections are
unchanged.
Only an extra generated EOF blank line was normalized.
- clang-format v16, check-format, build hygiene and git diff --check.
- Changed-line clang-tidy against the full PR base, with the previously
documented local resource-dir correction and comment-only VFS overlay
for an existing unmatched NOLINTEND in unmodified core/types.h.
Neither workaround changes repository source.
- No mixed-version cluster or non-x86_64 platform validation was run
locally.
- Behavior changed: No (execution is optimized, numeric semantics are
retained).
- Does this need documentation: No
---
be/src/exprs/function/math.cpp | 23 +++
be/test/exprs/function/function_math_test.cpp | 193 ++++++++++++++++++++-
.../data/function_p0/test_pow_square.out | 79 +++++++++
.../suites/function_p0/test_pow_square.groovy | 114 ++++++++++++
4 files changed, 402 insertions(+), 7 deletions(-)
diff --git a/be/src/exprs/function/math.cpp b/be/src/exprs/function/math.cpp
index d25cca3a901..8bd849549ca 100644
--- a/be/src/exprs/function/math.cpp
+++ b/be/src/exprs/function/math.cpp
@@ -15,6 +15,8 @@
// specific language governing permissions and limitations
// under the License.
+#include <algorithm>
+#include <cfenv>
#include <cstddef>
#include <cstdint>
#include <cstring>
@@ -643,6 +645,27 @@ private:
auto& a = column_left_ptr->get_data();
auto& c = column_result->get_data();
size_t size = a.size();
+ if constexpr (std::is_same_v<Impl, PowImpl>) {
+ if (column_right_ptr->template get_value<Impl::type>() == 2.0
&&
+ std::fegetround() == FE_TONEAREST) {
+ // Integer bases up to 2^26 have exact binary64 squares
(at most 2^52).
+ // Other bases can differ by one ULP between
multiplication and libm.
+ // Stop checking at the first unsafe value rather than
checking the whole block.
+ const auto exact_end = std::ranges::find_if_not(a,
[](double value) {
+ return std::abs(value) <= 0x1p26 && value ==
std::trunc(value);
+ });
+ const auto exact_rows = static_cast<size_t>(exact_end -
a.begin());
+ for (size_t i = 0; i < exact_rows; ++i) {
+ c[i] = a[i] * a[i];
+ }
+ // Keep the remaining rows on libm, not compiler-folded
pow(x, 2).
+ volatile double exponent = 2.0;
+ for (size_t i = exact_rows; i < size; ++i) {
+ c[i] = Impl::apply(a[i], exponent);
+ }
+ return column_result;
+ }
+ }
for (size_t i = 0; i < size; ++i) {
c[i] = Impl::apply(a[i], column_right_ptr->template
get_value<Impl::type>());
}
diff --git a/be/test/exprs/function/function_math_test.cpp
b/be/test/exprs/function/function_math_test.cpp
index 318ca61dc72..2e2196f226e 100644
--- a/be/test/exprs/function/function_math_test.cpp
+++ b/be/test/exprs/function/function_math_test.cpp
@@ -15,20 +15,37 @@
// specific language governing permissions and limitations
// under the License.
+#include <array>
+#include <bit>
+#include <cfenv>
#include <climits>
+#include <cmath>
#include <cstdint>
#include <limits>
+#include <memory>
+#include <numbers>
#include <random>
+#include <span>
#include <string>
+#include <utility>
+#include <vector>
+#include "core/block/block.h"
#include "core/column/column_const.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_vector.h"
#include "core/data_type/data_type_decimal.h"
+#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_string.h"
+#include "core/field.h"
#include "core/types.h"
#include "exprs/function/function_test_util.h"
+#include "exprs/function/simple_function_factory.h"
+#include "exprs/function_context.h"
#include "testutil/any_type.h"
#include "testutil/column_helper.h"
+#include "util/defer_op.h"
namespace doris {
@@ -297,16 +314,178 @@ TEST(MathFunctionTest, log_test) {
}
TEST(MathFunctionTest, pow_test) {
- std::string func_name = "pow"; // pow(x,y)
+ const InputTypeSet input_types = {TYPE_DOUBLE, TYPE_DOUBLE};
+ const double inf = std::numeric_limits<double>::infinity();
+ const double nan = std::numeric_limits<double>::quiet_NaN();
+ const DataSet data_set = {{{10.0, 1.0}, 10.0}, {{10.0, 10.0},
10000000000.0},
+ {{100.0, -2.0}, 0.0001}, {{2.0, 0.5},
std::numbers::sqrt2},
+ {{-2.0, 3.0}, -8.0}, {{-2.0, 0.5}, nan},
+ {{nan, 0.0}, 1.0}, {{1.0, nan}, 1.0},
+ {{0.0, -2.0}, inf}, {{-0.0, -3.0}, -inf},
+ {{Null(), 2.0}, Null()}, {{2.0, Null()},
Null()}};
+ check_function_all_arg_comb<DataTypeFloat64, true>("pow", input_types,
data_set);
+}
- InputTypeSet input_types = {PrimitiveType::TYPE_DOUBLE,
PrimitiveType::TYPE_DOUBLE};
+static void check_pow_square_result(const IColumn& result, std::span<const
double> values,
+ bool nullable, bool const_base) {
+ ASSERT_EQ(result.size(), values.size());
+ const auto& nested =
+ nullable ? assert_cast<const
ColumnNullable&>(result).get_nested_column() : result;
+ const auto& data = assert_cast<const ColumnFloat64&>(nested).get_data();
+ // Keep the reference call on libm rather than letting the compiler turn
pow(x, 2) into x * x.
+ volatile double exponent = 2.0;
+ for (size_t i = 0; i < values.size(); ++i) {
+ const bool expect_null = nullable && !const_base && i == values.size()
- 1;
+ if (nullable) {
+ EXPECT_EQ(result.is_null_at(i), expect_null);
+ }
+ if (expect_null) {
+ continue;
+ }
+ const double base = values[const_base ? 0 : i];
+ const double expected = std::pow(base, exponent);
+ if (std::isnan(expected)) {
+ EXPECT_TRUE(std::isnan(data[i]));
+ } else {
+ EXPECT_EQ(std::bit_cast<uint64_t>(data[i]),
std::bit_cast<uint64_t>(expected))
+ << "row=" << i << " base=" << base;
+ }
+ }
+}
+
+static void check_pow_square_column_shapes(const std::string& name, bool
nullable, int const_mask,
+ std::span<const double> values) {
+ SCOPED_TRACE(testing::Message()
+ << name << " nullable=" << nullable << " const_mask=" <<
const_mask);
+ const size_t rows = values.size();
+ DataTypePtr type = std::make_shared<DataTypeFloat64>();
+ if (nullable) {
+ type = make_nullable(type);
+ }
+ auto bases = type->create_column();
+ for (double value : values) {
+ bases->insert(Field::create_field<TYPE_DOUBLE>(value));
+ }
+ if (nullable) {
+ bases->pop_back(1);
+ bases->insert_default();
+ }
+ auto exponents = type->create_column();
+ exponents->insert(Field::create_field<TYPE_DOUBLE>(2.0));
+ ColumnPtr left = std::move(bases);
+ if (const_mask & 1) {
+ left = ColumnConst::create(left->clone_resized(1), rows);
+ }
+ ColumnPtr right = ColumnConst::create(exponents->get_ptr(), rows);
+ if (!(const_mask & 2)) {
+ right = right->convert_to_full_column_if_const();
+ }
+ Block block({{left, type, "base"}, {right, type, "exponent"}});
+ auto function = SimpleFunctionFactory::instance().get_function(
+ name, block.get_columns_with_type_and_name(), type);
+ ASSERT_NE(function, nullptr);
+ block.insert({nullptr, type, "result"});
+ FunctionUtils fn_utils(type, {type, type}, false);
+ auto* context = fn_utils.get_fn_ctx();
+ ASSERT_TRUE(function->open(context, FunctionContext::FRAGMENT_LOCAL).ok());
+ ASSERT_TRUE(function->open(context, FunctionContext::THREAD_LOCAL).ok());
+ const auto status = function->execute(context, block, {0, 1}, 2, rows);
+ EXPECT_TRUE(function->close(context, FunctionContext::THREAD_LOCAL).ok());
+ EXPECT_TRUE(function->close(context,
FunctionContext::FRAGMENT_LOCAL).ok());
+ ASSERT_TRUE(status.ok()) << status.to_string();
+ auto result =
block.get_by_position(2).column->convert_to_full_column_if_const();
+ check_pow_square_result(*result, values, nullable, const_mask & 1);
+}
- DataSet data_set = {{{10.0, 1.0}, 10.0},
- {{10.0, 10.0}, 10000000000.0},
- {{100.0, -2.0}, 0.0001},
- {{2.0, 0.5}, 1.4142135623730951}};
+static void check_pow_square_all_shapes(std::span<const double> values) {
+ for (const auto* name : {"pow", "power", "dpow", "fpow"}) {
+ for (int const_mask = 0; const_mask < 4; ++const_mask) {
+ check_pow_square_column_shapes(name, false, const_mask, values);
+ check_pow_square_column_shapes(name, true, const_mask, values);
+ }
+ }
+}
- static_cast<void>(check_function<DataTypeFloat64, true>(func_name,
input_types, data_set));
+TEST(MathFunctionTest, pow_square_column_shapes) {
+ const double inf = std::numeric_limits<double>::infinity();
+ // The first value differs by one ULP between libm pow(x, 2) and x * x.
Keep it first
+ // so that the constant-base cases also exercise it; approximate equality
would miss this.
+ const std::array values = {1.1500729535343723e-17,
+ -1.5,
+ 0.0,
+ -0.0,
+ 1.0,
+ -2.0,
+ 0.5,
+ 12345.125,
+ 1e154,
+ 1e-154,
+ std::numeric_limits<double>::max(),
+ std::numeric_limits<double>::min(),
+ std::numeric_limits<double>::denorm_min(),
+ inf,
+ -inf,
+ std::numeric_limits<double>::quiet_NaN(),
+ 3.0};
+ check_pow_square_all_shapes(values);
+}
+
+TEST(MathFunctionTest, pow_square_exact_integers) {
+ std::vector<double> values = {3.0, -3.0, 0x1p26, -0x1p26};
+ for (int value = -4096; value <= 4096; ++value) {
+ values.push_back(value);
+ }
+ std::mt19937_64 random(0);
+ for (size_t i = 0; i < 4096; ++i) {
+ values.push_back(static_cast<double>(random() % ((1ULL << 27) + 1)) -
0x1p26);
+ }
+ check_pow_square_all_shapes(values);
+}
+
+TEST(MathFunctionTest, pow_square_integer_boundaries) {
+ // A safe prefix followed by out-of-range integers and fractional
neighbours.
+ // 94906297 has a one-ULP square difference between libm and
multiplication on some platforms.
+ std::vector<double> values = {3.0, -3.0, 0x1p26,
+ -0x1p26, 94906297.0, -94906297.0,
+ 0x1p27, -0x1p27, 1.1500729535343723e-17};
+ for (int offset = -32; offset <= 32; ++offset) {
+ const double value = 0x1p26 + offset;
+ values.insert(values.end(),
+ {value, -value, std::nextafter(value, 0.0),
+ std::nextafter(value,
std::numeric_limits<double>::infinity())});
+ }
+ check_pow_square_all_shapes(values);
+}
+
+TEST(MathFunctionTest, pow_square_empty_block) {
+ for (const auto* name : {"pow", "power", "dpow", "fpow"}) {
+ for (int const_mask = 0; const_mask < 4; ++const_mask) {
+ check_pow_square_column_shapes(name, false, const_mask, {});
+ }
+ }
+}
+
+TEST(MathFunctionTest, pow_square_random_bits) {
+ std::mt19937_64 random(1);
+ std::vector<double> values;
+ values.reserve(4096);
+ for (size_t i = 0; i < 4096; ++i) {
+ values.push_back(std::bit_cast<double>(random()));
+ }
+ check_pow_square_all_shapes(values);
+}
+
+TEST(MathFunctionTest, pow_square_rounding_modes) {
+ std::fenv_t environment;
+ ASSERT_EQ(std::fegetenv(&environment), 0);
+ Defer restore_environment([&] { EXPECT_EQ(std::fesetenv(&environment), 0);
});
+ // All values are in the fast domain, so only the rounding-mode guard can
disable it.
+ const std::array values = {3.0, -3.0, 0.0, -0.0, 0x1p26, -0x1p26, 0x1p26 -
1};
+ for (int mode : {FE_TONEAREST, FE_UPWARD, FE_DOWNWARD, FE_TOWARDZERO}) {
+ SCOPED_TRACE(testing::Message() << "rounding_mode=" << mode);
+ ASSERT_EQ(std::fesetround(mode), 0);
+ check_pow_square_all_shapes(values);
+ }
}
TEST(MathFunctionTest, ceil_test) {
diff --git a/regression-test/data/function_p0/test_pow_square.out
b/regression-test/data/function_p0/test_pow_square.out
new file mode 100644
index 00000000000..27da9ba2ff1
--- /dev/null
+++ b/regression-test/data/function_p0/test_pow_square.out
@@ -0,0 +1,79 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !square_aliases --
+0 4 4 4 4 4
+1 2.25 2.25 2.25 2.25 2.25
+2 1 1 1 1 1
+3 0.25 0.25 0.25 0.25 0.25
+4 0 0 0 0 0
+5 0.25 0.25 0.25 0.25 0.25
+6 1 1 1 1 1
+7 2.25 2.25 2.25 2.25 2.25
+8 4 4 4 4 4
+
+-- !nullable_square --
+0 \N \N \N
+1 9 \N 9
+2 4 \N 4
+3 1 \N 1
+4 0 \N 0
+5 1 \N 1
+6 4 \N 4
+7 9 \N 9
+8 16 \N 16
+
+-- !column_shapes --
+0 1 1 4
+1 2 1 4
+2 4 4 4
+3 8 27 4
+4 16 256 4
+
+-- !square_shape_equality --
+0 true true true true
+1 true true true true
+2 false false false false
+3 false false false false
+
+-- !exact_integer_square --
+0 0 0 0 0 0
+1 100000000000000 100000000000000 100000000000000 100000000000000
100000000000000
+2 400000000000000 400000000000000 400000000000000 400000000000000
400000000000000
+3 900000000000000 900000000000000 900000000000000 900000000000000
900000000000000
+4 1600000000000000 1600000000000000 1600000000000000
1600000000000000 1600000000000000
+5 2500000000000000 2500000000000000 2500000000000000
2500000000000000 2500000000000000
+6 3600000000000000 3600000000000000 3600000000000000
3600000000000000 3600000000000000
+
+-- !integer_square_boundaries --
+0 4503599090499600 4503599090499600 4503599090499600
4503599090499600 true
+1 4503599224717321 4503599224717321 4503599224717321
4503599224717321 true
+2 4503599358935044 4503599358935044 4503599358935044
4503599358935044 true
+3 4503599493152769 4503599493152769 4503599493152769
4503599493152769 true
+4 4503599627370496 4503599627370496 4503599627370496
4503599627370496 true
+5 4503599761588225 4503599761588225 4503599761588225
4503599761588225 true
+6 4503599895805956 4503599895805956 4503599895805956
4503599895805956 true
+7 4503600030023689 4503600030023689 4503600030023689
4503600030023689 true
+8 4503600164241424 4503600164241424 4503600164241424
4503600164241424 false
+9 4503600298459161 4503600298459161 4503600298459161
4503600298459161 false
+
+-- !out_of_range_square_shapes --
+0 true true true true
+1 true true true true
+2 false false false false
+3 false false false false
+
+-- !other_exponents --
+0 1 -2 0.25 -8 NaN
+1 1 -1 1 -1 NaN
+2 1 0 Infinity 0 0
+3 1 1 1 1 1
+4 1 2 0.25 8 1.4142135623730951
+
+-- !square_boundaries --
+0 NaN NaN
+1 Infinity Infinity
+2 Infinity Infinity
+3 0 0
+4 Infinity Infinity
+5 0 0
+6 1e+308 1e+308
+7 1e-308 1e-308
diff --git a/regression-test/suites/function_p0/test_pow_square.groovy
b/regression-test/suites/function_p0/test_pow_square.groovy
new file mode 100644
index 00000000000..1ede6381b22
--- /dev/null
+++ b/regression-test/suites/function_p0/test_pow_square.groovy
@@ -0,0 +1,114 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_pow_square") {
+ qt_square_aliases """
+ select number, pow(x, 2.0), power(x, 2.0), dpow(x, 2.0), fpow(x, 2.0),
x * x
+ from (select number, cast(number - 4 as double) / 2 as x
+ from numbers("number" = "9")) t
+ order by number
+ """
+
+ qt_nullable_square """
+ select number, pow(x, 2.0), pow(x, cast(null as double)), x * x
+ from (select number, if(number = 0, null, cast(number - 4 as double))
as x
+ from numbers("number" = "9")) t
+ order by number
+ """
+
+ qt_column_shapes """
+ select number, pow(2.0, x), pow(x, x), pow(-2.0, 2.0)
+ from (select number, cast(number as double) as x
+ from numbers("number" = "5")) t
+ order by number
+ """
+
+ // The first two rows have an exponent of 2, but y remains a vector
because later rows
+ // have an exponent of 3. These bases expose a one-ULP difference between
pow(x, 2) and x * x.
+ // Derive x from number to keep alias evaluation in BE rather than FE
constant folding.
+ // Do not guard the equality with y = 2: predicate inference can simplify
it away.
+ qt_square_shape_equality """
+ select number,
+ pow(x, 2.0) = pow(x, y),
+ power(x, 2.0) = power(x, y),
+ dpow(x, 2.0) = dpow(x, y),
+ fpow(x, 2.0) = fpow(x, y)
+ from (
+ select number,
+ cast(2 * number - 1 as double)
+ * cast('1.1500729535343723e-17' as double) as x,
+ if(number < 2, 2.0, 3.0) as y
+ from numbers("number" = "4")
+ ) t order by number
+ """
+
+ qt_exact_integer_square """
+ select number, pow(x, 2.0), power(x, 2.0), dpow(x, 2.0), fpow(x, 2.0),
x * x
+ from (
+ select number,
+ cast(number * 10000000 as double) * if(number % 2 = 0, 1,
-1) as x
+ from numbers("number" = "7")
+ ) t order by number
+ """
+
+ qt_integer_square_boundaries """
+ select number, pow(x, 2.0), power(x, 2.0), dpow(x, 2.0), fpow(x, 2.0),
+ pow(x, 2.0) = pow(x, y)
+ from (
+ select number,
+ cast(number + 67108860 as double) * if(number % 2 = 0, 1,
-1) as x,
+ if(number < 8, 2.0, 3.0) as y
+ from numbers("number" = "10")
+ ) t order by number
+ """
+
+ // Integer bases alone are not sufficient: these squares are not exactly
representable.
+ qt_out_of_range_square_shapes """
+ select number,
+ pow(x, 2.0) = pow(x, y), power(x, 2.0) = power(x, y),
+ dpow(x, 2.0) = dpow(x, y), fpow(x, 2.0) = fpow(x, y)
+ from (
+ select number, cast(2 * number - 1 as double) * 94906297.0 as x,
+ if(number < 2, 2.0, 3.0) as y
+ from numbers("number" = "4")
+ ) t order by number
+ """
+
+ qt_other_exponents """
+ select number, pow(x, 0.0), pow(x, 1.0), pow(x, -2.0), pow(x, 3.0),
pow(x, 0.5)
+ from (select number, cast(number - 2 as double) as x
+ from numbers("number" = "5")) t
+ order by number
+ """
+
+ qt_square_boundaries """
+ select number, pow(x, 2.0), x * x
+ from (
+ select number, case number
+ when 0 then cast('nan' as double)
+ when 1 then cast('inf' as double)
+ when 2 then cast('-inf' as double)
+ when 3 then cast('-0.0' as double)
+ when 4 then cast('1e308' as double)
+ when 5 then cast('1e-308' as double)
+ when 6 then cast('1e154' as double)
+ when 7 then cast('1e-154' as double)
+ end as x
+ from numbers("number" = "8")
+ ) t order by number
+ """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]