mirror of
https://github.com/opencv/opencv.git
synced 2026-07-26 22:03:04 +04:00
f968fb969f
* experimental new arithmetics; work-in-progress * continue working on new-gen arithmetic expressions * * improved performance of the new add on small arrays * added sub * extended tests * fixed potential bug when adding multi-channel array and a single-channel scalar * improved const handling * * added copyMask * added mul/dev (without scale so far) * * accelerated mul * addedd scale to mul and div * * done substantial refactoring; however a few more rounds of refactoring are ahead. * added min, max, absdiff, addweighted. * improved performance of the new arithmetic functions, but some of them are still slow, e.g. operations with mask have some bugs (that affect speed, not accuracy). * * further (significantly) accelerated several functions, especially on small arrays: mul, binary ops with mask * further polished the new arithmetic engine * started integration of the new element-wise arithmetic engine into core * big step forward. We now use the new engine inside cv::add, subtract, multiply, divide, absdiff, min and max. * big progress: * added bitwise operations * fixed and accelerated compare * ported regression tests to test new broadcasting behaviour of arithmetic functions * lot's of improvements in compare, divide, addWeighted! * lot's of small and big performance improvements in the new arithmetics * * some more optimizations; parsing texpr-expressions is now faster as well * port new_arithm to Linux/x86: dispatch guards, scalar-Mat compat fallback, dnn shape-contract fixes Core: - arithm.simd.hpp: CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY guards (the file is included once per dispatched mode on x86), vx_load_expand instead of the 128-bit v_load_expand, VTraits::vlanes() instead of ::nlanes - arithm.cpp/precomp.hpp: compat fallback for scalar-like Mat operands (1x1, 1xcn/cnx1, 4x1 CV_64F - java/python tuples, operator-(Mat, Matx)): treated as a per-channel scalar ONLY when the shapes are not broadcast-compatible, so every valid numpy-style broadcast keeps its meaning and calls that would otherwise throw get the 4.x semantics DNN (fallout of the stricter shape semantics, found by the new engine): - dict.hpp: DictValue relied on fresh AutoBuffer having size()==fixed_size; allocate explicitly - batch_norm: weights_/bias_ are 1-D [n] now; 0/1-D forward runs on exact-shape 1-D views - net_impl2: extend the post-forward sanity check to non-temp outputs - a layer that reallocates its preallocated output tensor now fails loudly instead of silently detaching the result from the graph - LSTM/LSTM2 batchwise (layout=1): getMemoryShapes now matches what forward() writes (ONNX: Y=(batch,seq,dirs,hid), Yh/Yc=(batch,dirs,hid)); forward assembles seq-major results in a local buffer and transposes INTO the preallocated outputs in place Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * u8/s8 multiply: exact integer SIMD path on non-FP16 builds (3-10x vs 5.x) The unit-scale branch of vecBinaryKernel already supported a separate work-vector type Wvec1 (used by the ARM f16 build and by u16/s16 everywhere), but on x86 the u8/s8 same-type multiply still went through the f32 hub. Route it through v_uint16/v_int16: products of 8-bit values fit exactly (255^2 < 2^16), the saturating pack on store gives bit-exact results at half the vector traffic. Also fix a latent kernel bug this exposed: the unit-scale branch stepped by Wvec's lane count while loading/storing Wvec1 vectors. All previous Wvec1 instantiations had equal lane counts, but u8's v_uint16 has 2x the lanes of v_float32 - the pairs overlapped (50% redundant work) and the tail backoff could write VECSZ bytes past the row end. The branch now derives its step, offsets and tail condition from Wvec1 itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * vecBinaryKernel: constexpr Op::useScalar instead of a runtime-only scale check Every binary op functor now declares whether it consumes the scale scalar (params[0]): true only for mul and the two div variants. Ops that ignore it (add/sub/min/max/absdiff) take the fast 2-arg branch unconditionally - the 'scalar == 1' check used to fail for them (their params[0] is 0), sending them through the preproc branch, and the condition now folds at compile time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * restore cv::hal::mul8u as a wrapper over the element-wise engine The symbol is still declared in core/hal/hal.hpp and called directly by external code (the G-API fluid backend in opencv_contrib), but its implementation went away with the old arithm kernels. Forward it to getMulFunc(CV_8U, CV_8U) - with scale==1 it lands on the new exact integer SIMD path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * silence every new-arithm warning reported by CI (ARM64/Mac) and gcc 15 - arithm.cpp: bitwise_op_ocl and the actualScalarDepth/coerceTypes helpers are consumed only by the OpenCL paths - guard them with HAVE_OPENCL; haveScalar in cv::compare is read only inside CV_OCL_RUN - CV_UNUSED for OpenCL-less builds - arithm_expr.hpp: declare getBitwiseFunc/getNotFunc/getAddWeightedFunc next to the other per-op entry points (-Wmissing-prototypes in arithm.dispatch.cpp) - arithm.simd.hpp: define CV_SIMD_16F to 0 when FP16 SIMD is absent (-Wundef); {}-init the expandScalar staging buffers (-Wmaybe-uninitialized: they are fully written before use, but the compiler cannot prove it with runtime vector widths); rename the compare kernel's lambda parameter (-Wshadow) - arithm_expr.cpp: rename the exec tile-lambda's hot-field locals that shadowed TExpr members and outer locals (-Wshadow) - test_new_arithm_extensive.cpp: rename the name-generator lambdas' parameter shadowing the INSTANTIATE macro's own (-Wshadow), drop an unused variable Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * restore 4.x scalar semantics for the bindings' 4x1 CV_64F Scalar columns The python/java bindings materialize numbers and tuples as a 2-D 4x1 CV_64F Mat - or UMat, when the call carries UMat arguments. Three CI-reported python failures came from those pseudo-scalars reaching the engine as arrays: - absdiff(int_arr, 0): the (4,1) column is broadcast-COMPATIBLE with a 1-D array, so numpy semantics silently won - an outer-product f64 result instead of the int per-channel-scalar one; - subtract(u8 4x8x4, (40,)): same, by the rows==4 coincidence; - multiply(UMat, 2., dst=UMat): the scalar arrives as a UMAT, which the scalar detection did not recognize at all. isScalarArg now treats the exact bindings shape - 2-D 4x1 CV_64F single-channel Mat/UMat against a <=4-channel array - as a scalar UNCONDITIONALLY (a 1-D [4] array has dims==1 and still broadcasts). One exception, decided in arithm_op: when the partner is itself a tiny scalar-shaped array, both are honest data and ride the broadcast (compare(Mat 4x1, Mat 1x1) - issue #8999 - stays elementwise). Small-array discipline, this all runs per engine call: the probes read Mat/UMat fields directly (rows == 4 alone rejects almost everything, no _InputArray getter dispatch), and a UMAT scalar's 32 bytes are copied into a caller-stack buffer - no heap, no getMat mapping. Measured: no latency change on 4x4/16x16 element-wise calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * cv::texpr: std::string_view -> const std::string& in the public API string_view in an exported signature breaks some CUDA toolchain builds, and for short expression strings the difference is immaterial (SSO, parsed once). The parser internals keep string_view - the argument converts implicitly. Also drop the now-unused <string_view> include from cvstd.hpp, so the header does not reach every nvcc TU. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * compare boundary-rewrite: fixed 4-slot kind/bound arrays instead of AutoBuffers A CONST operand is capped at 4 channels (addConst), so the per-channel kind/bound staging needs no dynamic buffers - plain int[4]/double[4], with a CV_Assert on the contract. This is also what gcc's -Wmaybe-uninitialized was flagging (it could not see the AutoBuffer's inline storage get filled). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * element-wise engine: unary math kernels (sqrt/exp/log/sin/cos/tanh/erf/relu) + select New dispatched pair math.simd.hpp / math.dispatch.cpp - the unary/ternary sibling of arithm.simd.hpp: - vecUnaryKernel: T -> T over f16/bf16/f32/f64 on top of the intrin_math primitives (v_exp/v_log/v_sin/v_cos/v_sqrt/v_erf/v_max). f32/f64 compute natively, f16/bf16 ride the f32 hub inside the kernel (vx_load_pair_as / v_store_pair_as) - no materialized casts. Continuity collapse + the halide right-edge backoff, suppressed in-place (it would re-apply Op to already-written values). tanh = (e^2x-1)/(e^2x+1) with the input clamped to +/-10 (f32) / +/-20 (f64) - unclamped saturation hits inf/inf = NaN. erf has no f64 SIMD primitive: std::erf per lane. - selectKernel(mask, x, y): 1-byte mask expanded to lane width and tested against zero in the INTEGER domain (immune to DAZ/FTZ), branches of any depth by element size, broadcast branches supported. - emitUnary: math over a float input is T -> T now (f16 in -> f16 out, native kernel when input and result depths match); integer inputs still compute in the float domain and land in f32. - emitTernary/select: literal branches are typed via typedConstFrom (an OP_CAST of a depth-less flex const crashed); a non-1-byte mask is normalized by an explicit "mask != 0" compare, never a value cast. texpr already parsed the function names - they now execute. Tests: per-depth accuracy of all 8 ops against the double std:: reference, integer input, in-place, select over 4 depths / const branch / float mask. Perf vs the classic kernels (1920x1080 f32, 16 threads, AVX2): exp 3.7x, log 2.6x, sqrt 5.7x faster; polarToCart expressed as (r*cos(a), r*sin(a)) 4.0x. Accuracy improves too (max rel err vs f64 reference): exp 8.1e-8 vs 2.1e-7, log 8.0e-8 vs 1.5e-7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * engine: OP_COPY_MASK folded into OP_SELECT; selectKernel moved to arithm.simd.hpp copyMask(dst, mask, src) is select(mask, src, dst) - one masking primitive instead of two. The compiler emits the masked-op tail as addInsn(OP_SELECT, mask, r, out, out): the output slot rides as both arg2 and the result, so unmasked elements are preserved by reading them back through the b-branch. OP_COPY_MASK, copyMaskKernel and getCopyMaskFunc are gone. selectKernel (moved from math.simd.hpp to arithm.simd.hpp) inherits every copyMaskKernel optimization: - the interleaved multichannel fast path (2..4 channels under a per-pixel mask: expand the mask once per VECSZ rows, v_store_interleave across lanes); - the per-row scalar path with the row-skip when the selected source row IS dst (the "leave the output untouched" half of copyMask); - plus the select-specific ones: branch broadcasts (stepx == 0) and the right-edge tail backoff under dst-aliases-a-branch - safe because re-running select over already-blended elements is idempotent; only dst == mask keeps the backoff off (the store would rewrite mask bytes before the re-read). Masked-add perf is on par with the old copyMask (1280x720, 1 thread: 8UC3 204 -> 198 us, 32FC3 1395 -> 1344, 8UC1/32FC1 within noise). Full core suite 24117 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * engine: dedicated vectorized pow kernel (moved from arithm to math.simd.hpp) pow was the last scalar-only binary op (scalarBinaryKernel + std::pow, f32/f64 only). The new powKernel keeps exact std::pow semantics and is T x T -> T over all four float depths (f16/bf16 via the f32 hub): - scalar exponent (the dominant call shape - texpr literals ride as 0-dim broadcast consts) is dispatched PER ROW to the special cases: y==2 -> x*x, y==3 -> x*x*x, y==0.5 -> v_sqrt, y==1 -> copy, y==0 -> fill 1; - everything else - including a per-element exponent array - runs the general vectorized exp(y * log(x)) path, valid for x > 0; a vector pair containing any x <= 0 lane falls back to scalar std::pow for that pair (v_check_any), which preserves every std::pow subtlety: signed results for integer y on negative bases, NaN for fractional y, the x == 0 family; - no right-edge tail backoff: pow is not idempotent, in-place calls finish rows in the scalar tail. Perf vs the classic cv::pow (1920x1080 f32, 16 threads, AVX2): p=2 1.4x (classic special-cases it too), p=3 6.1x, p=0.5 6.3x, fractional p 4.5x with slightly better accuracy (6.1e-7 vs 7.4e-7 max rel err). Tests: exponent sweep 2/3/0.5/1/0/2.5/-1.5 vs the double std::pow reference on f32/f64, negative bases (exact signed cubes, NaN for fractional), array exponent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * powKernel: halide right-edge tail backoff in every SIMD loop Same shape as vecBinaryKernel: the final partial vector re-processes [width - VECSZ*2, width) instead of finishing scalar, suppressed when dst aliases an input (pow is not idempotent - the overlap region must be recomputed from an untouched source, which the no-alias case guarantees). Modest measured win (~2% on ROI rows for the special-cased exponents; the general path tail was already cheap - modern libm powf is fast), no regressions; mainly aligns the kernel with the house style, where every SIMD loop ends vector-wide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix MSVC 2019 C2975: function-local constexpr as a template argument inside a lambda MSVC 2019 loses the constexpr-ness of function-local constants (LOCAL_OPS, MAX_DIMS, ...) when they are used as template arguments inside a lambda body (AutoBuffer<Slice, LOCAL_OPS> / std::array<int, MAX_DIMS> in the parallel bodies of BroadcastOp::run and TExpr::exec). Hoist them to namespace scope - no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ocl_arithm_op: route 16U multiply to the CPU engine on Apple OpenCL The Apple OpenCL driver miscompiles the 16U multiply kernel: products near the top of the u16 range come back wrapped instead of saturated (CPU vs GPU NORM_INF up to 65535 in OCL_Arithm/Mul.Mat CV_16U cases). The same arithm.cl kernel is correct on Intel NEO and NVIDIA drivers - verified not to reproduce on Linux/Intel iGPU - so gate the decline to __APPLE__ only; the CPU engine computes 16u multiply exactly (integer SIMD path). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * engine: neg/abs as compositions, clamp kernel, ** and ?: operators, abs(a-b) peephole - OP_NEG and OP_ABS need no kernels: neg = sub(0, a), abs = absdiff(a, 0) - including the engine absdiff auto-type rule (signed |a| lands in the UNSIGNED type of the same width: |SHRT_MIN| fits u16 exactly instead of saturating; NB the public cv::absdiff auto depth keeps the source type for 4.x compatibility - values agree, the depth rule is the engine own). - peephole: abs(x - y) rewrites to absdiff(x, y) ALWAYS. On integers the literal semantics differ (the subtract saturates first: u8 gives max(x-y, 0)), but whoever writes abs(a - b) means absdiff - we deliberately hand out the useful semantics instead of the saturation artifact. The just- emitted OP_SUB is retired via the moveToOutput manoeuvre, so the program shrinks to the single absdiff instruction. abs(x), abs(x - 0) and absdiff(x, 0) all give one result. - OP_CLAMP kernel (arithm.simd.hpp): v_min(v_max(x, lo), hi) over u8/s8/u16/s16/u32/s32/f32 (+f64 with 64-bit SIMD), scalar f16/bf16/64-bit ints; lo/hi may broadcast (the common clamp(img, a, b) shape) or be full arrays; the tail backoff stays on under dst-aliases-x (clamp is idempotent). emitTernary types literal bounds via typedConstFrom (same flex-const crash select had) and keeps the auto result type pinned to x. - parser: "a ** b" == pow(a, b), precedence above * /, RIGHT-associative (a ** 2 ** 3 == a ** 8); "cond ? a : b" == select(cond, a, b), precedence below everything, right-associative chains (f1 ? a : f2 ? b : c) work without parentheses. parseTernary() is the expression entry point now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * cv::exp/log/sqrt on the engine via math_op; hal functions wrapped as engine kernels math_op is the master function of the unary math family (the arithm_op analogue): same-shape same-type output over f16/bf16/f32/f64 (classic exp/log accepted f32/f64 only - the half floats are new), two tiers: - small (<= 100000 elements) and continuous: call the kernel DIRECTLY over the flattened data - no TExpr, no broadcastOp, no parallel_for setup; - everything else: the usual single-instruction program via compile()/exec() (parallelism for large arrays, real steps for ROIs). getMathFunc routes OP_EXP/OP_LOG at f32/f64 through the full cv::hal stack - an external vendor HAL (CALL_HAL), IPP, or the built-in table kernels, whichever is installed - by wrapping hal::exp32f/exp64f/log32f/log64f as engine kernels with the function pointer in TKernel::userdata, the same mechanism castKernel uses for core BinaryFuncs. The engine adds tiling and parallelism on top, so every tier gets the best available scalar-span implementation. v_exp/v_log remain for f16/bf16 (the f32 hub) and the ops hal has no entry points for. v_log_default_32f: the degree-8 polynomial is evaluated by Estrin pairing (4 dependent levels) instead of an 8-FMA Horner chain (~5% on the f16 hub path). An exp64 Taylor-without-division rewrite was tried and benched SLOWER than the Cephes Pade scheme (the evaluation is FMA-throughput-bound, and vdivpd pipelines well enough) - reverted; a table-based reduction is the only way further there. cv::exp f32 (16 threads, AVX2+IPP build), old -> new: 640 elements 0.16 -> 0.15 us, 16k 2.79 -> 2.49, 640x480 47 -> 20, 1920x1080 452 -> 60 us (the old CPU loops were single-threaded); f64 exp 1080p 1295 -> 161 us. No size regresses; small arrays now run at installed-HAL speed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * getMathFunc: IPP tier + raw-HAL probing; the exp/log table kernels are deleted The raw cv_hal_* entry points return int for a reason: without an installed HAL they are stubs returning CV_HAL_ERROR_NOT_IMPLEMENTED. getMathFunc now selects the exp/log implementation in three tiers: 1. HAVE_IPP && ipp::useIPP(): ippsExp/ippsLn through thin int adapters (IPP is not routed through the cv_hal_ hooks, so it needs its own tier); 2. the raw cv_hal_exp32f/... hook, PROBED once with a 1-element call on the safe input 1.0 (cached in magic statics): implemented -> wrapped as an engine kernel with the function pointer in TKernel::userdata; 3. the engine own v_exp/v_log kernels. Whichever wins, the engine adds tiling and parallelism on top. The EXPTAB/LOGTAB table kernels and their tables (~790 lines in mathfuncs_core.simd.hpp + mathfuncs.cpp) are DELETED: they benched within ~15% of v_exp/v_log, not worth a second implementation. The public cv::hal::exp32f/exp64f/log32f/log64f keep their contract - CALL_HAL, then IPP, then the built-in implementation - but the built-in is now the engine vector kernel via ew::mathSpanEngine (one contiguous span, exported from math.dispatch.cpp). All unary math kernels (vec/scalar/hal wrappers, pow) also handle the vertical-broadcast tile (s0y == 0, a row expanded into a matrix): the first row is computed, the rest are memcpy of it - transcendentals cost far more than a row copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * texpr: hypot(x, y) binary op (alias: mag) hypot = sqrt(x^2 + y^2), NAIVE like cv::magnitude (not the overflow-safe std::hypot), computed in the float work type; kernels for the four float depths only (T x T -> T; integer inputs ride the usual f32-compute + cast). A 10-line EwHypot functor on top of vecBinaryKernel in arithm.simd.hpp - broadcast branches, continuity collapse and the tail backoff come for free. Registered in the parser as both "hypot" (the C/numpy name) and "mag" (the cv::magnitude-flavored alias). A building block for the future cartToPolar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * texpr: atan2(y, x) binary op - radians, standard C range v_atan2 (arithm.simd.hpp, generic over the universal-intrinsic float vector): the fastAtan2 minimax polynomial from mathfuncs_core v_atan_f32 reworked to plain radians - the 180/pi factor dropped from the coefficients and the C quadrant logic instead of the [0, 360) wrap, so the result matches std::atan2 over (-pi, pi]. Measured absolute accuracy ~1.6e-4 rad. (v_atan_f32 itself is untouched - cv::phase/fastAtan2 keep their degree semantics.) EwAtan2 rides vecBinaryKernel: f16/bf16/f32 through v_atan2 (the f32 hub), f64 through exact scalar std::atan2. arg0 = y, arg1 = x, like std::atan2; float depths only, same emitBinary policy as pow/hypot. Parser name "atan2". Together with hypot this completes the cartToPolar building blocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix the RISC-V RVV build and two ARM64 warnings The new f64 kernel registrations (hypot, pow, the unary math family) gate on CV_SIMD_64F || CV_SIMD_SCALABLE_64F, but the vx_setall_as(const double*, v_float64&) helper family in arithm.simd.hpp was still CV_SIMD_64F-only - scalable platforms (RVV) have v_float64 with CV_SIMD_64F == 0, so vecBinaryKernel<double, ...> failed to instantiate there. Widen the helper gate to match (verified with a riscv64 rv64gcv cross-build of opencv_core - the engine f64 paths now vectorize on RVV instead of not compiling). cv::exp/cv::log: the depth local is consumed by CV_OCL_RUN only - CV_UNUSED for OpenCL-less builds (ARM64 -Wunused-variable). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * silence the remaining ARM64 gcc warnings - compare boundary-rewrite: {}-init the fixed kind/bound arrays (filled for every channel used below, but gcc cannot prove it across the cn <= 4 loop); - cv::exp/log: [[maybe_unused]] on the depth local (consumed by CV_OCL_RUN only), instead of the CV_UNUSED idiom; - AutoBuffer::reserve: a targeted -Wmaybe-uninitialized suppression around the live-element copy loop - only [0, sz) is read, all written before, but gcc inlining a grow-from-inline-storage chain cannot see that. An annotation for the analyzer, no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * saturating 32-bit add/sub kernels; cv::texpr python binding; two CI warnings - v_add_sat/v_sub_sat for v_int32/v_uint32, local to arithm.simd.hpp for now (the plan is to grow them into proper universal intrinsics later): NEON single-instruction vqadd/vqsub, elsewhere the Hacker Delight bit tricks over universal intrinsics (u32 add is 2 ops: or with the wrapped-compare mask). EwAdd/EwSub overload vec() for the 32-bit lanes and getAddSubFunc routes 32S/32U T->T through vecBinaryKernel instead of the former pure scalar kernel. Semantics unchanged - the scalar int64 tail already saturated; directed boundary tests added (both rails, 0 - INT_MIN, u32 cases, a full-range random block vs an exact int64 reference). 640x480 32S add: 0.48x of 5.x -> parity (memory-bound); 1080p: 6-8x. - cv::texpr becomes CV_EXPORTS_W: python gets cv.texpr(expr, [inputs]) -> tuple of ndarrays, so `res, = cv.texpr(...)` and `mag, ang = cv.texpr(...)` unpacking both work. modules/python/test/test_expr.py covers arithmetic, the fused abs(a-b), casts, broadcasting, ?: and ** operators, math functions vs numpy, clamp, named temporaries, tuple outputs, the one-line cartToPolar and the int32 saturation cases. - warnings: {}-init the parser args array (gcc -Wmaybe-uninitialized on Ubuntu 20/22); the compare short-row block gates sizeof(T) <= 4 as constexpr so the f64 instantiation does not leave set-but-unused locals (gcc 9 -Wunused-but-set-variable). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * arithm_op: direct-kernel fast path for small continuous arrays Building and compiling the 1-instruction program plus the BroadcastOp setup costs ~40-250ns per call - negligible on big images, dominant at 127x61-class sizes where the classic 5.x functions were 1.3-2x faster. Mirror math_op two tiers in arithm_op: two same-type same-shape continuous arrays, no mask, no scalar, result depth == input depth, <= 100k elements -> call the T x T -> T kernel directly over the flattened elements (checks ordered cheapest-first). Applies to add/subtract/min/max/absdiff/multiply/addWeighted/and/or/xor; compare and divide lower to more than a single kernel (boundary rewrites, int guards) and keep the ordinary path. addWeighted falls through automatically for the 32/64-bit int types whose lowering is wide-compute + cast (getElemwiseFunc returns no direct kernel there). 127x61 vs 5.x, was -> now: add/subtract 8UC1 0.76x -> 1.3x, min/max u8 0.6x -> ~1x, addWeighted 1.0x -> 1.1-1.4x (32SC1 stays 4.5x); the one remaining laggard is add/sub 32SC1 (0.74-0.82x) - the price of the new SATURATING semantics (7-instruction AVX2 emulation vs the wrapping single add of 5.x; single-instruction on NEON). The 127x61 size is ADDED PERMANENTLY to the arithmetic/addWeighted/compare perf grids: per-call overhead regressions in these base functions must be caught by CI, not discovered by users. dst creation goes through createSameSize (whole-shape transfer including layout and future metadata, not piecemeal dims+sizes) here and in math_op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * cv::pow rebuilt on the engine; integer-exponent and 1/sqrt(x) kernel branches Routing: p = 0/1/2 keep their early special cases (fill/copy/multiply); an INTEGER array with an INTEGER power keeps the classic iPow multiply chain - bit-exact compatibility, including its wrap-around quirks (iPow squares in int, so e.g. pow(255,4) on u8 wraps negative and saturates to 0 - somebody may rely on that). Everything else goes through the engine with the math_op two-tier scheme: small continuous arrays call powKernel directly (the exponent rides as a broadcast T scalar), the rest run the tiled parallel program. Integer arrays with fractional powers compute in the float domain and saturate back; the 32U/64-bit depths (classic iPow asserted on them) and f16/bf16 (the classic float path misread them) now just work. powKernel gets two new per-row exponent branches: - p == -0.5: 1/v_sqrt(x) (the classic path used IPP ippsInvSqrt_A21, a 21-bit approximation; ours is exact - slightly slower on small arrays, 4.5x faster at 1080p via parallelism); - any other INTEGER p (|p| <= 65536): LSB-first binary exponentiation, the same multiply chain and order as iPow, fully vectorized - a few ulp accurate vs ~2e-7 of exp(p*log x), and exact on non-positive bases (the sign falls out of the multiplies, 0^negative divides to inf) - no scalar patching. cv::pow f32 vs 5.x: p=0.5 365 -> 61 us at 1080p (6x), p=3 7.7x, p=5 7x AND faster at every size (the old scalar chain: 0.81 -> 0.47 us at 127x61), p=2.5 5.3x. The s16^5 iPow path is untouched (118.7 == 118.5 us). pow_exponents accuracy tests extended to 11 exponents x f32/f64 against the double std::pow reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf: SANITY_CHECK_NOTHING for the tests whose grids got the 127x61 size The 127x61 entry added to guard per-call overhead has no regression data in opencv_extra, so the legacy SANITY_CHECK in addWeighted/compare failed on CI (locally it passes silently without the test-data path). Accuracy of both functions is covered by the accuracy suite; the perf tests should measure time. PatchNaNs/finiteMask keep their SANITY_CHECK - their grids are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix a temp-buffer double release in the liveness pass ("d*d" crash) When the same temp is passed as SEVERAL arguments of its last-use instruction (e.g. the named-intermediate expression "d = {0} - {1}; d*d", where the MUL consumes slot d twice), the buffer-reuse scan pushed the temp's physical buffer onto the free list once per argument. That overflows the ntemps-sized freeBufs array (caught by the AutoBuffer range check in Debug: python test_expr.py::test_named_temporary) and, in larger programs, would hand the same physical buffer to two live temps. Release the buffer once by retiring lastUse[t] after the first hit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * executor: recalibrate opCost to vectorized cycles + clamp the stripe count The per-element op costs fed into the parallel_for_ stripe hint were scalar-era estimates (~20x above the vectorized reality: the atan2/exp polynomials run at ~1.5 cycles/element, not 30). The hint therefore split transcendental/divide work into hundreds of ~1 us jobs, which the macOS/GCD backend dispatches poorly under sustained load, on top of the P/E-core equal-share straggler effect. Measured on M4 Max (12P+4E), sustained medians @1920x1080 f32: texpr atan2 320 -> 133 us, cv::exp 280 -> 141 us, cv::log 301 -> ~200 us, cv::pow(x,2.5) 388 -> 376 us; the PR tables' math rows improved ~1.5-2x across the board. - opCost is now in units of ~1/4 cycle/element of the SIMD kernels: cheap ops 1 (unchanged), div/sqrt/hypot/convert_scale 10 -> 3, transcendentals 30 -> 6. - the stripe hint is clamped by min(4*nthreads, max(32, 3*nthreads)): ~4 stripes/thread is plenty of granularity for element-wise work, and the ceiling is 32 pieces except on machines with many (heterogeneous) cores, where anything coarser than ~3 pieces/worker turns the slow cores into equal-share bottlenecks (measured: 32 stripes on 16 threads is the worst point of the curve - 193 us vs 137 us at 48 for atan2). getNumThreads() is clamped from below (WINRT/plugin backends may report 0). Not addressed here (needs cross-machine data, M2/M3 Ultra): streaming memory-bound ops saturate the M4 Max fabric at ~8 fat stripes and E-core participation only adds contention - a cost model cannot express that; candidate follow-up is a bytes-aware clamp or a GCD-backend-level fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * multiply: v_mul_sat integer kernels (full product clamped to the type) v_mul_sat(V, V) -> V for u8/s8/u16/s16/u32/s32 - the full-precision product clamped to the lane type, which is exactly cv::multiply's integer semantics at scale == 1. Local to arithm.simd.hpp for now, next to v_add_sat/v_sub_sat, to be promoted into proper universal intrinsics later. NEON: widening vmull + saturating narrow (vqmovn); other backends: the portable v_mul_expand + saturating v_pack composition for 8/16-bit lanes. 32-bit lanes have no universal widening multiply (no v_mul_expand for s32), so the 32-bit integer fast path is NEON-only for now and the other backends keep the previous f64 work-vector kernels (which measure well on x86 with IPP-free AVX2). EwMul::vec() now routes the integer lane types through v_mul_sat, and getMulFunc_ passes the NATIVE lane vector as the scale==1 fast-path type: whole-register loads/stores, the widening happens inside the multiply. Replaces both the half-register widening loads (u8/s8/u16/s16) and the scalar-equivalent f64 path for 32S/32U on NEON. M4 Max, 640x480 (the sizes where the old kernels lost to carotene): 8U 22.0 -> 13.6 us, 8S 15.0 -> 11.1, 16S 21.2 -> 17.4, 32S 84.8 -> 30.5 (parity with the classic path everywhere, 32S was 0.38x). 1920x1080: 8S 1.30x -> 1.78x, 16S 2.57x -> 2.98x, 32S 2.20x -> 4.02x vs 5.x. Correctness: exhaustive 8-bit (all 65536 pairs per sign), directed saturation corners for 16/32-bit (46341^2, INT_MIN*-1, 65536*65536, all sign combinations) against an exact int64 reference. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * NEON: make v_cvt_f64(v_int32) exact (was via f32, losing bits > 2^24) The NEON implementations of v_cvt_f64/v_cvt_f64_high for v_int32 did s32 -> f32 -> f64 (vcvt_f32_s32 + vcvt_f64_f32), silently rounding any |x| > 2^24. Every vectorized f64 work path with int32 inputs on AArch64 was affected: the engine's addWeighted/divide 32S kernels, convertTo 32S -> 64F, etc. Found via addWeighted 32SC1 on values ~1e9: max error was 32 vs the exact double reference (the classic carotene path is worse still - it computes in f32 end-to-end with f32-truncated weights, max error 96 on the same data). The exact sequence sxtl + scvtf (vmovl_s32 + vcvtq_f64_s64) is the same 2 instructions, so there is no cost. addWeighted 32SC1 on the engine now matches the exact-double reference bit-for-bit and stays at parity/1.4x vs the classic path (640x480/1920x1080). Pre-existing upstream bug (same code in 4.x) - worth a standalone backport with directed large-value tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * math_op: drop the temporary direct-IPP tier for exp/log Upstream moved the IPP math wrappers into the hal/ipp HAL module (cv_hal_exp32f/ log32f & co now resolve to ipp_hal_* which honor cv::ipp::useIPP via CV_HAL_CHECK_USE_IPP). The engine's single probeHalUnary(cv_hal_*) probe already picks that up uniformly, so the stopgap #ifdef HAVE_IPP ippExp/ippLog tier and its ipp::useIPP() branch in getMathFunc are now redundant - removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * arithm: restore cv::hal::and8u/or8u/xor8u/not8u as engine wrappers These public CV_EXPORTS entry points (core/hal/hal.hpp) lost their definitions when the bitwise ops moved to the element-wise engine, but they are still declared and called by other modules (opencv_objdetect's aruco) and external code - the link broke with undefined references to cv::hal::and8u/xor8u. Restore them as thin forwarders over the engine's byte-wise bitwise kernels (getBitwiseFunc / getNotFunc), mirroring the existing mul8u wrapper. CV_Assert guards the kernel lookup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * blobdetector: guard empty contour when computing blob radius The new AutoBuffer leaves its tail uninitialized for trivial types, which surfaced a -Wmaybe-uninitialized in findBlobs where the median of per-point distances is read. Use a std::vector, default the radius to 0, and compute the median only for a non-empty contour - no unproven size invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
379 lines
17 KiB
C++
379 lines
17 KiB
C++
// This file is part of OpenCV project.
|
|
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
|
// of this distribution and at http://opencv.org/license.html.
|
|
|
|
// Ad-hoc perf comparison for the element-wise engine vs classic cv::add. Lives in
|
|
// opencv_test_core for now (move to opencv_perf_core later). Each (type-combo, size) is run
|
|
// 10..30 times; the minimum getTickCount() time is reported as the most stable metric.
|
|
|
|
#include "perf_precomp.hpp"
|
|
|
|
// TODO: these ad-hoc micro-benchmarks call the engine internals (cv::ew) directly and print min-times
|
|
// by hand. They are DISABLED (#if 0) pending a rewrite onto the perf framework (PERF_TEST_P over the
|
|
// PUBLIC cv::add/... ops), measured against a separate 5.x build with opencv_perf_core + the summary
|
|
// script. Kept here so the intended coverage is not lost.
|
|
#if 0
|
|
#include "../src/arithm_expr.hpp"
|
|
#include <iostream>
|
|
#include <iomanip>
|
|
|
|
namespace opencv_test { namespace {
|
|
|
|
using namespace cv::ew;
|
|
|
|
static Mat randMat(const std::vector<int>& shape, int cn, int depth, double lo, double hi)
|
|
{
|
|
Mat m64((int)shape.size(), shape.data(), CV_MAKETYPE(CV_64F, cn));
|
|
cvtest::randUni(theRNG(), m64, Scalar::all(lo), Scalar::all(hi));
|
|
Mat m; m64.convertTo(m, CV_MAKETYPE(depth, cn));
|
|
return m;
|
|
}
|
|
|
|
// single-channel 0/1 mask of the given spatial shape
|
|
static Mat randMask(const std::vector<int>& shape)
|
|
{
|
|
Mat m((int)shape.size(), shape.data(), CV_8U);
|
|
cvtest::randUni(theRNG(), m, Scalar::all(0), Scalar::all(2));
|
|
return m;
|
|
}
|
|
|
|
// Min over `iters` trials of the per-call time in MICROSECONDS. Each trial runs f() `ninner`
|
|
// times inside one timed region and divides by ninner, so the timer's coarse resolution is
|
|
// amortized across many calls - essential for sub-microsecond operations.
|
|
template<typename F>
|
|
static double minUs(F&& f, int iters, int ninner)
|
|
{
|
|
f(); // warmup (allocates reused output, warms caches)
|
|
double best = DBL_MAX;
|
|
for (int i = 0; i < iters; i++)
|
|
{
|
|
int64 t0 = getTickCount();
|
|
for (int j = 0; j < ninner; j++) f();
|
|
double us = (getTickCount() - t0) * 1e6 / getTickFrequency() / ninner;
|
|
best = std::min(best, us);
|
|
}
|
|
return best;
|
|
}
|
|
|
|
struct Combo { int da, db, Tr; std::string name; };
|
|
struct Sz { std::vector<int> shape; int cn; int ninner; const char* name; };
|
|
|
|
// Shared add/sub sweep over (type-combo x size). `masked` adds a single-channel write-mask: the
|
|
// engine builds the op-into-temp + copyMask program; the cv:: reference times cv::add/subtract with
|
|
// the mask (only for same-type combos - mixed-type + mask isn't compared). Engine correctness is
|
|
// always checked against a deterministic zero + copyTo(mask) reference.
|
|
static void perfBinOp(TOp op, const char* title, bool masked)
|
|
{
|
|
const std::string opname = opName(op);
|
|
const Combo combos[] = {
|
|
{ CV_8U, CV_8U, CV_8U, opname + "(u8, u8)->u8 " },
|
|
{ CV_16F, CV_16F, CV_16F, opname + "(f16, f16)->f16 " },
|
|
{ CV_32F, CV_32F, CV_32F, opname + "(f32, f32)->f32 " },
|
|
{ CV_8U, CV_16F, CV_16F, opname + "(u8, f16)->f16 " },
|
|
};
|
|
const Sz sizes[] = {
|
|
{ {10,10,10}, 1, 5000, "10x10x10 " },
|
|
{ {165,121}, 1, 2000, "165x121 " },
|
|
{ {1024,1024}, 3, 4, "1024x1024x3 " },
|
|
};
|
|
|
|
std::cout << "\n[ew-perf] " << title << " (min us per call over 30 trials)\n";
|
|
std::cout << " combo size engine cv::op speedup\n";
|
|
std::cout << " -----------------------------------------------------------------\n";
|
|
|
|
for (const Combo& c : combos)
|
|
for (const Sz& s : sizes)
|
|
{
|
|
Mat a = randMat(s.shape, s.cn, c.da, 0, 100);
|
|
Mat b = randMat(s.shape, s.cn, c.db, 0, 100);
|
|
// div: make the divisor nonzero (the 0..100 data includes 0 for integer b); float-div by
|
|
// zero is UB, integer-div by zero is a separate (accuracy-tested) corner not timed here.
|
|
if (op == OP_DIV) { Mat b64; b.convertTo(b64, CV_64F); b64.setTo(1.0, b64 == 0.0); b64.convertTo(b, c.db); }
|
|
Mat mask = masked ? randMask(s.shape) : Mat();
|
|
Mat init = masked ? randMat(s.shape, s.cn, c.Tr, 0, 50) : Mat(); // pre-existing dst
|
|
Mat in2[] = {a, b}, in3[] = {a, b, mask}, out = masked ? init.clone() : Mat();
|
|
Mat* inps = masked ? in3 : in2;
|
|
const int mdepth = masked ? CV_8U : EW_DEPTH_NONE;
|
|
// u8*u8 mul: exercise a realistic non-unit scale (1/255, the normalized-blend case) -
|
|
// checks the specialized u8 mul branch still holds up when scale != 1.
|
|
const double scale = (op == OP_MUL && c.da == CV_8U && c.db == CV_8U) ? 1.0/255 : 1.0;
|
|
|
|
// Full per-call path (matches a future cv:: op): build the program every call.
|
|
// (masked preserves the pre-filled `out` where mask==0, so repeated calls are idempotent.)
|
|
double te = minUs([&]{ TExpr p; makeBinaryArithProgram(p, op, c.da, c.db, c.Tr, mdepth, scale);
|
|
p.exec(inps, &out); }, 30, s.ninner);
|
|
|
|
// engine correctness sanity: add/sub vs cv:: directly; mul/div vs a double reference (the
|
|
// extensive test owns exactness, so a generous tolerance here just guards against garbage).
|
|
const bool fp = (op == OP_MUL || op == OP_DIV);
|
|
Mat ref;
|
|
if (op == OP_ADD) cv::add (a, b, ref, noArray(), c.Tr);
|
|
else if (op == OP_SUB) cv::subtract(a, b, ref, noArray(), c.Tr);
|
|
else if (op == OP_MIN || op == OP_MAX || op == OP_ABSDIFF) {
|
|
Mat aT, bT; a.convertTo(aT, c.Tr); b.convertTo(bT, c.Tr);
|
|
if (op == OP_MIN) cv::min(aT, bT, ref);
|
|
else if (op == OP_MAX) cv::max(aT, bT, ref);
|
|
else cv::absdiff(aT, bT, ref); }
|
|
else { Mat aD, bD, q; a.convertTo(aD, CV_64F); b.convertTo(bD, CV_64F);
|
|
if (op == OP_MUL) cv::multiply(aD, bD, q, scale); else cv::divide(aD, bD, q);
|
|
q.convertTo(ref, c.Tr); }
|
|
if (masked) { Mat full = ref; ref = init.clone(); full.copyTo(ref, mask); }
|
|
double n = cvtest::norm(out, ref, NORM_INF);
|
|
double sc = std::max(1.0, cvtest::norm(ref, NORM_INF));
|
|
double tol = (c.Tr==CV_16F||c.Tr==CV_16BF) ? (fp ? 1e-2*sc : 1.0)
|
|
: c.Tr==CV_32F ? (fp ? 1e-3*sc : 1e-3)
|
|
: (fp ? 1.0 : 0.0);
|
|
EXPECT_LE(n, tol) << title << " " << c.name << " " << s.name;
|
|
|
|
// cv:: timing reference (skip for mixed-type masked / mul / div, where the cv:: array op
|
|
// needs same-type inputs). min/max/absdiff have no dtype arg and require identical input
|
|
// types, so they are only timed against cv:: when da == db.
|
|
const bool mm = (op == OP_MIN || op == OP_MAX || op == OP_ABSDIFF);
|
|
double tc = -1;
|
|
if (c.da == c.db || (!masked && !fp && !mm))
|
|
{
|
|
Mat tmp;
|
|
InputArray m = masked ? InputArray(mask) : noArray();
|
|
if (op == OP_ADD) tc = minUs([&]{ cv::add (a, b, tmp, m, c.Tr); }, 30, s.ninner);
|
|
else if (op == OP_SUB) tc = minUs([&]{ cv::subtract(a, b, tmp, m, c.Tr); }, 30, s.ninner);
|
|
else if (op == OP_MUL) tc = minUs([&]{ cv::multiply(a, b, tmp, scale, c.Tr); }, 30, s.ninner);
|
|
else if (op == OP_MIN) tc = minUs([&]{ cv::min (a, b, tmp); }, 30, s.ninner);
|
|
else if (op == OP_MAX) tc = minUs([&]{ cv::max (a, b, tmp); }, 30, s.ninner);
|
|
else if (op == OP_ABSDIFF) tc = minUs([&]{ cv::absdiff(a, b, tmp); }, 30, s.ninner);
|
|
else tc = minUs([&]{ cv::divide (a, b, tmp, 1.0, c.Tr); }, 30, s.ninner);
|
|
}
|
|
|
|
std::cout << " " << c.name << " " << s.name << " "
|
|
<< std::fixed << std::setprecision(3) << std::setw(8) << te << " ";
|
|
if (tc >= 0)
|
|
std::cout << std::setw(8) << tc << " " << std::setprecision(2) << std::setw(6) << (tc/te) << "x";
|
|
else
|
|
std::cout << " - - ";
|
|
std::cout << "\n";
|
|
}
|
|
}
|
|
|
|
// Compare sweep over the same (type-combo x size) grid as perfBinOp, but the result is a u8 boolean
|
|
// mask with the SAME shape and channel count as the inputs (a per-element compare, not a reduction).
|
|
// The engine builds the compare program (auto rdepth = u8 mask). cv::compare is timed for context only
|
|
// on the same-type single-channel combos (it needs identical input types). Correctness is checked
|
|
// per channel against a compare in f64 (exact for the small [0,16] data).
|
|
static void perfCompare(TOp op, const char* title)
|
|
{
|
|
const std::string opname = opName(op);
|
|
const int cmpop = (op == OP_CMP_EQ) ? cv::CMP_EQ : cv::CMP_GT;
|
|
const Combo combos[] = {
|
|
{ CV_8U, CV_8U, CV_8U, opname + "(u8, u8) ->u8 " },
|
|
{ CV_16F, CV_16F, CV_8U, opname + "(f16, f16)->u8 " },
|
|
{ CV_32F, CV_32F, CV_8U, opname + "(f32, f32)->u8 " },
|
|
{ CV_8U, CV_16F, CV_8U, opname + "(u8, f16)->u8 " },
|
|
};
|
|
const Sz sizes[] = {
|
|
{ {10,10,10}, 1, 5000, "10x10x10 " },
|
|
{ {165,121}, 1, 2000, "165x121 " },
|
|
{ {1024,1024}, 3, 4, "1024x1024x3 " },
|
|
};
|
|
|
|
std::cout << "\n[ew-perf] " << title << " (min us per call over 30 trials)\n";
|
|
std::cout << " combo size engine cv::cmp speedup\n";
|
|
std::cout << " -----------------------------------------------------------------\n";
|
|
|
|
for (const Combo& c : combos)
|
|
for (const Sz& s : sizes)
|
|
{
|
|
Mat a = randMat(s.shape, s.cn, c.da, 0, 16); // small range so EQ fires often
|
|
Mat b = randMat(s.shape, s.cn, c.db, 0, 16);
|
|
Mat in2[] = {a, b}, out;
|
|
|
|
double te = minUs([&]{ TExpr p; makeBinaryArithProgram(p, op, c.da, c.db, -1);
|
|
p.exec(in2, &out); }, 30, s.ninner);
|
|
|
|
// correctness: per-channel compare in f64 -> 0/255 (engine's default mask value)
|
|
std::vector<Mat> ach, bch; cv::split(a, ach); cv::split(b, bch);
|
|
std::vector<Mat> refch(s.cn);
|
|
for (int ch = 0; ch < s.cn; ch++) {
|
|
Mat af, bf; ach[ch].convertTo(af, CV_64F); bch[ch].convertTo(bf, CV_64F);
|
|
cv::compare(af, bf, refch[ch], cmpop);
|
|
}
|
|
Mat ref; cv::merge(refch, ref);
|
|
ASSERT_EQ(out.type(), CV_8UC(s.cn)) << title << " " << c.name;
|
|
EXPECT_EQ(0, cvtest::norm(out, ref, NORM_INF)) << title << " " << c.name << " " << s.name;
|
|
|
|
double tc = -1;
|
|
if (c.da == c.db) // cv::compare needs identical input types (it handles multi-channel)
|
|
{
|
|
Mat tmp;
|
|
tc = minUs([&]{ cv::compare(a, b, tmp, cmpop); }, 30, s.ninner);
|
|
}
|
|
|
|
std::cout << " " << c.name << " " << s.name << " "
|
|
<< std::fixed << std::setprecision(3) << std::setw(8) << te << " ";
|
|
if (tc >= 0)
|
|
std::cout << std::setw(8) << tc << " " << std::setprecision(2) << std::setw(6) << (tc/te) << "x";
|
|
else
|
|
std::cout << " - - ";
|
|
std::cout << "\n";
|
|
}
|
|
}
|
|
|
|
TEST(Core_EW_Perf, cmpEQ)
|
|
{
|
|
perfCompare(OP_CMP_EQ, "cmpEQ");
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, cmpGT)
|
|
{
|
|
perfCompare(OP_CMP_GT, "cmpGT");
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, add)
|
|
{
|
|
perfBinOp(OP_ADD, "add", false);
|
|
|
|
// per-channel scalar broadcast: (1024x1024) 8UC3 + (1x1) 8UC3 -> exercises the broadcast path
|
|
Mat a = randMat({1024,1024}, 3, CV_8U, 0, 100);
|
|
Mat b = randMat({1,1}, 3, CV_8U, 0, 100);
|
|
Mat inps[] = {a, b}, out;
|
|
|
|
double te = minUs([&]{ TExpr p; makeBinaryArithProgram(p, OP_ADD, CV_8U, CV_8U, CV_8U, EW_DEPTH_NONE, 1.);
|
|
p.exec(inps, &out); }, 30, 4);
|
|
|
|
Vec3b bv = b.at<Vec3b>(0, 0);
|
|
Scalar sb(bv[0], bv[1], bv[2]);
|
|
Mat ref;
|
|
double tc = minUs([&]{ cv::add(a, sb, ref); }, 30, 4);
|
|
EXPECT_EQ(0.0, cvtest::norm(out, ref, NORM_INF)) << "u8+scalar broadcast";
|
|
|
|
std::cout << "\n[ew-perf] addScalar u8 +u8 ->u8 1024x1024x3 + (1x1)x3 "
|
|
<< std::fixed << std::setprecision(3) << std::setw(8) << te << " "
|
|
<< std::setw(8) << tc << " " << std::setprecision(2) << std::setw(6) << (tc/te) << "x\n";
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
// Diagnostic: split the per-call cost into program BUILD (makeXxx + compile) vs EXEC (the run).
|
|
// For each op we time (build+exec) and (exec-only, program built once). The gap = build overhead,
|
|
// which a program cache would remove. 165x121 c1 (small, so overhead dominates).
|
|
TEST(Core_EW_Perf, buildVsExec)
|
|
{
|
|
const std::vector<int> shape{165,121};
|
|
std::cout << "\n[ew-perf] build-vs-exec 165x121 c1 (min us per call over 30 trials)\n";
|
|
std::cout << " op build+exec exec-only build cv::\n";
|
|
std::cout << " --------------------------------------------------------------\n";
|
|
|
|
auto row = [&](const char* name, TOp op, int da, int db, int Tr,
|
|
std::function<void()> cvref)
|
|
{
|
|
Mat a = randMat(shape, 1, da, 1, 100);
|
|
Mat b = randMat(shape, 1, db, 1, 100);
|
|
if (op == OP_DIV) { Mat t; b.convertTo(t, CV_64F); t.setTo(1.0, t==0.0); t.convertTo(b, db); }
|
|
Mat in[] = {a, b}, out;
|
|
|
|
double tFull = minUs([&]{ TExpr p; makeBinaryArithProgram(p, op, da, db, Tr); p.exec(in, &out); }, 30, 2000);
|
|
TExpr p; makeBinaryArithProgram(p, op, da, db, Tr);
|
|
double tExec = minUs([&]{ p.exec(in, &out); }, 30, 2000);
|
|
double tBuild = minUs([&]{ TExpr q; makeBinaryArithProgram(q, op, da, db, Tr); }, 30, 2000);
|
|
double tcv = cvref ? minUs(cvref, 30, 2000) : -1;
|
|
|
|
std::cout << " " << std::left << std::setw(18) << name << std::right << std::fixed << std::setprecision(3)
|
|
<< std::setw(8) << tFull << " " << std::setw(8) << tExec << " "
|
|
<< std::setw(7) << tBuild << " ";
|
|
if (tcv >= 0) std::cout << std::setw(7) << tcv; else std::cout << " -";
|
|
std::cout << "\n";
|
|
};
|
|
|
|
Mat tmp;
|
|
Mat a8 = randMat(shape,1,CV_8U,1,100), b8 = randMat(shape,1,CV_8U,1,100);
|
|
row("add u8->u8", OP_ADD, CV_8U, CV_8U, CV_8U, [&]{ cv::add(a8,b8,tmp); });
|
|
row("mul u8->u8", OP_MUL, CV_8U, CV_8U, CV_8U, [&]{ cv::multiply(a8,b8,tmp); });
|
|
row("div u8->u8", OP_DIV, CV_8U, CV_8U, CV_8U, nullptr);
|
|
Mat af = randMat(shape,1,CV_32F,1,100), bf = randMat(shape,1,CV_32F,1,100);
|
|
row("mul f32->f32", OP_MUL, CV_32F, CV_32F, CV_32F, [&]{ cv::multiply(af,bf,tmp); });
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, sub)
|
|
{
|
|
perfBinOp(OP_SUB, "sub", false);
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, mul)
|
|
{
|
|
perfBinOp(OP_MUL, "mul", false);
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, div)
|
|
{
|
|
perfBinOp(OP_DIV, "div", false);
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, min)
|
|
{
|
|
perfBinOp(OP_MIN, "min", false);
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, max)
|
|
{
|
|
perfBinOp(OP_MAX, "max", false);
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, absdiff)
|
|
{
|
|
perfBinOp(OP_ABSDIFF, "absdiff", false);
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, addMask)
|
|
{
|
|
perfBinOp(OP_ADD, "add+mask", true);
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, subMask)
|
|
{
|
|
perfBinOp(OP_SUB, "sub+mask", true);
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
TEST(Core_EW_Perf, addWeighted)
|
|
{
|
|
// fused: addWeighted(a,alpha,b,beta,gamma) = a*alpha + b*beta + gamma. Two convert_scale MACs +
|
|
// an add => 2 temp buffers => exercises the body's L1 column-fragmentation.
|
|
std::cout << "\n[ew-perf] addWeighted (min us per call over 30 trials)\n";
|
|
std::cout << " combo size engine cv::aW speedup\n";
|
|
|
|
const double alpha = 1.5, beta = -0.75, gamma = 12.0;
|
|
struct Sz2 { std::vector<int> shape; int cn; int ninner; const char* name; };
|
|
const Sz2 sizes2[] = {
|
|
{ {10,10,10}, 1, 2000, "10x10x10 " },
|
|
{ {165,121}, 1, 1000, "165x121 " },
|
|
{ {1024,1024}, 1, 8, "1024x1024 " },
|
|
};
|
|
for (const Sz2& s : sizes2)
|
|
{
|
|
Mat a = randMat(s.shape, s.cn, CV_32F, -100, 100);
|
|
Mat b = randMat(s.shape, s.cn, CV_32F, -100, 100);
|
|
Mat inps[] = {a, b}, out;
|
|
double te = minUs([&]{ TExpr p; makeAddWeightedProgram(p, CV_32F, CV_32F, CV_32F, alpha, beta, gamma);
|
|
p.exec(inps, &out); }, 30, s.ninner);
|
|
Mat ref;
|
|
double tc = minUs([&]{ cv::addWeighted(a, alpha, b, beta, gamma, ref); }, 30, s.ninner);
|
|
EXPECT_LE(cvtest::norm(out, ref, NORM_INF), 1e-2) << "addWeighted " << s.name;
|
|
|
|
std::cout << " f32 aW->f32 " << s.name << " " << std::fixed << std::setprecision(3)
|
|
<< std::setw(8) << te << " " << std::setw(8) << tc << " "
|
|
<< std::setprecision(2) << std::setw(6) << (tc/te) << "x\n";
|
|
}
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
}} // namespace
|
|
|
|
#endif
|