On Sat Oct 24, 2026 at 2:40 PM PDT, Sarvesh Chandra wrote:
> The AVX-512 masked epilogue emits a masked load (UNSPEC_MASKLOAD under
> a VEC_MERGE) feeding a masked binary op; the wrapper hides the memory
> operand from combine, so it is not folded into the operation.
>
> Add define_insn_and_split patterns that strip the wrapper.  The masked
> off lanes of the load are zero, so the correctness of a fold depends on
> the value of the result lanes that are masked off:
>
>  - When an outer mask is present, the whole result is wrapped in
>    vec_merge (op (...), const0, mask), so the off lanes are forced to
>    zero regardless of the operator.  Both a commutative and a
>    non-commutative pattern are provided for this case and accept every
>    operator.
>
>  - When there is no outer mask, the fold forces the result off lanes to
>    zero, which only matches the operator when 0 op x == 0.  This holds
>    for mult, and and umin, so the unmasked commutative pattern is
>    restricted to those via the new zero_absorbing_operator
>    predicate.  No unmasked non-commutative pattern is provided: for
>    minus x - 0 == x and for div x / 0 is undefined, so neither is safe.
>
> Iterator V1248FH_AVX512VLBW covers all maskable modes.
>
> gcc/ChangeLog:
>
>       PR target/123997
>       * config/i386/predicates.md (sub_div_operator):
>       New predicate.
>       (zero_absorbing_operator): New predicate.
>       * config/i386/sse.md (V1248FH_AVX512VLBW): New mode iterator.
>       (*comm_maskload_fold_unmasked_op): New pattern.
>       (*comm_maskload_fold_masked_op): New pattern.
>       (*noncomm_maskload_fold_masked_op): New pattern.
>
> gcc/testsuite/ChangeLog:
>
>       PR target/123997
>       * gcc.target/i386/avx512-maskload-fold-1.c: New test.
>       * gcc.target/i386/avx512-maskload-fold-2.c: New test.
>
> Co-authored-by: Venkataramanan Kumar <[email protected]>
> Signed-off-by: Sarvesh Chandra <[email protected]>
> ---
>
> Hi Hongtao,
>
> Post our discussion at the Cauldron, where we discussed how vec_merge
> (op ((mem), reg), 0, k) already has matching patterns in sse.md, doesn't
> need a vec_merge (op (UNSPEC(mem), reg), 0, k) and a mem->reg conversion
> would require a load so the match would always succeed, and safety is
> guaranteed by the EVEX standard for these select instructions, I feel
> this is the minimal patch that enforces both correctness and compactness
> without an explosion of patterns. The more complex cases like vpavgb I
> feel could be handled in GCC 18, through a combiner patch.
>
> Regards,
> Sarvesh
>
>> Rather than describe what I want, let me show it.  Below is a two-pattern
>> demo I built and tested on trunk.  It is FP add only -- deliberately
>> minimal, not a patch -- but it demonstrates the structure I would like v2
>> to follow: keep UNSPEC_MASKLOAD, no target hook, no op-specific pattern
>> explosion.
>>
>> The key point is that you do not need to strip the UNSPEC to fold the
>> memory operand.  Combine already hands recog the folded form; all that is
>> missing is a define_insn of that shape.
>>
>> 1. The form the PR needs.
>> --------------------------
>>
>> The MEM sits *inside* the UNSPEC and carries the "m" constraint, exactly
>> as *<avx512>_load<mode>_mask already does.  That is what makes the RA
>> question disappear: LRA never sees a bare MEM in an operator slot, so
>> there is nothing for it to reload, and the UNSPEC still records that the
>> access is masked so nothing downstream can widen it.
>>
>> (define_insn "*add<mode>3_maskload_fold_maskz"
>>   [(set (match_operand:VFH_AVX512VL 0 "register_operand" "=v")
>>     (vec_merge:VFH_AVX512VL
>>       (plus:VFH_AVX512VL
>>         (unspec:VFH_AVX512VL
>>           [(match_operand:VFH_AVX512VL 1 "memory_operand" "m")]
>>           UNSPEC_MASKLOAD)
>>         (match_operand:VFH_AVX512VL 2 "register_operand" "v"))
>>       (match_operand:VFH_AVX512VL 3 "const0_operand")
>>       (match_operand:<avx512fmaskmode> 4 "register_operand" "Yk")))]
>>   "TARGET_AVX512F"
>>   "vadd<ssemodesuffix>\t{%1, %2, %0%{%4%}%{z%}|%0%{%4%}%{z%}, %2, %1}"
>>   [(set_attr "type" "sseadd")
>>    (set_attr "prefix" "evex")
>>    (set_attr "mode" "<MODE>")])
>>
>> 2. Bridging what combine actually produces.
>> --------------------------------------------
>>
>> Combine does not hand us the flat form above.  It hands us the nested one,
>> with the inner vec_merge still in place, because simplify_merge_mask
>> refuses to strip it for a trapping MEM.  From the -1.c dump on trunk:
>>
>>   Failed to match this instruction:
>>   (set (reg:V8DF 133)
>>       (vec_merge:V8DF (plus:V8DF (vec_merge:V8DF (unspec:V8DF [
>>                           (mem:V8DF (plus:DI (mult:DI (reg:DI 129)
>>                                       (const_int 8))
>>                                   (reg/v/f:DI 120 [ a ])) [1  S64 A64])
>>                       ] UNSPEC_MASKLOAD)
>>                   (const_vector:V8DF [(const_double 0.0) repeated x8])
>>                   (reg:QI 118))
>>               (reg:V8DF 134))
>>           (const_vector:V8DF [(const_double 0.0) repeated x8])
>>           (reg:QI 118)))
>>
>> Since match_dup 3 forces both masks to be the same register, the outer
>> mask already zeroes the off lanes and the inner merge is redundant.  So
>> match that shape and split to the pattern above:
>>
>> (define_insn_and_split "*add<mode>3_maskload_fold_nested"
>>   [(set (match_operand:VFH_AVX512VL 0 "register_operand")
>>     (vec_merge:VFH_AVX512VL
>>       (plus:VFH_AVX512VL
>>         (vec_merge:VFH_AVX512VL
>>           (unspec:VFH_AVX512VL
>>         [(match_operand:VFH_AVX512VL 1 "memory_operand")]
>>         UNSPEC_MASKLOAD)
>>           (match_operand:VFH_AVX512VL 2 "const0_operand")
>>           (match_operand:<avx512fmaskmode> 3 "register_operand"))
>>         (match_operand:VFH_AVX512VL 4 "register_operand"))
>>       (match_operand:VFH_AVX512VL 5 "const0_operand")
>>       (match_dup 3)))]
>>   "TARGET_AVX512F && ix86_pre_reload_split ()"
>>   "#"
>>   "&& 1"
>>   [(set (match_dup 0)
>>     (vec_merge:VFH_AVX512VL
>>       (plus:VFH_AVX512VL
>>         (unspec:VFH_AVX512VL [(match_dup 1)] UNSPEC_MASKLOAD)
>>         (match_dup 4))
>>       (match_dup 5)
>>       (match_dup 3)))])
>>
>> Note the split *keeps* the UNSPEC.  That is the whole difference from your
>> version: we remove the redundant inner mask, not the record that the load
>> is masked.
>>
>> 3. Result.
>> -----------
>>
>> 45 lines, no other changes.  avx512-maskload-fold-1.c:
>>
>>   before:  vmovupd (%r9,%rax,8), %zmm2{%k1}{z}
>>            vmovupd (%r8,%rax,8), %zmm1{%k1}{z}
>>            vaddpd  %zmm1, %zmm2, %zmm0{%k1}{z}
>>
>>   after:   vmovupd (%r9,%rax,8), %zmm0{%k1}{z}
>>            vaddpd  (%r8,%rax,8), %zmm0, %zmm0{%k1}{z}
>>
>> pr97642-1.c still emits no k regs, and a maskz-load + maskz-add built from
>> intrinsics is unaffected.
>>
>> And op can be generalized to other operations, similar like your masked 
>> patterns.
>>
>> But for the those unmasked cases, GCC doesn't have per-lane analysis in the 
>> rtl to decide if the transformation for the unmasked patterns are valid.
>> Maybe in the vectorizer always generates MASKED_LOAD + COND_OP + 
>> MASKED_STORE instead MASKED_LOAD + OP + MASKED_STORE. Then the masked 
>> define_insn and define_insn_and_split can automatically matches that.
>>
>> BR,
>> Hongtao
>
>  gcc/config/i386/predicates.md                 | 10 ++
>  gcc/config/i386/sse.md                        | 96 +++++++++++++++++++
>  .../gcc.target/i386/avx512-maskload-fold-1.c  | 32 +++++++
>  .../gcc.target/i386/avx512-maskload-fold-2.c  |  8 ++
>  4 files changed, 151 insertions(+)
>  create mode 100644 gcc/testsuite/gcc.target/i386/avx512-maskload-fold-1.c
>  create mode 100644 gcc/testsuite/gcc.target/i386/avx512-maskload-fold-2.c
>
> diff --git a/gcc/config/i386/predicates.md b/gcc/config/i386/predicates.md
> index cf3a68a9fe8..c44eba44417 100644
> --- a/gcc/config/i386/predicates.md
> +++ b/gcc/config/i386/predicates.md
> @@ -1740,6 +1740,16 @@
>  (define_predicate "commutative_operator"
>    (match_code "plus,mult,and,ior,xor,smin,smax,umin,umax"))
>  
> +;; Return true for the non-commutative ops we fold (minus, div).
> +(define_predicate "sub_div_operator"
> +  (match_code "minus,div"))
> +
> +;; Return true for commutative operators that absorb zero, where
> +;; 0 op x == 0 (mult, and, umin).  Used to fold a zero-masked load into
> +;; the op when there is no outer mask: the masked-off lanes stay zero.
> +(define_predicate "zero_absorbing_operator"
> +  (match_code "mult,and,umin"))
> +
>  ;; Return true if OP is a binary operator that can be promoted to wider mode.
>  (define_predicate "promotable_binary_operator"
>    (ior (match_code "plus,minus,and,ior,xor,ashift")
> diff --git a/gcc/config/i386/sse.md b/gcc/config/i386/sse.md
> index 51d1e9b455a..039c8428c77 100644
> --- a/gcc/config/i386/sse.md
> +++ b/gcc/config/i386/sse.md
> @@ -2792,6 +2792,102 @@
>     (set_attr "btver2_decode" "direct,double")
>     (set_attr "mode" "<MODE>")])
>  
> +;; AVX-512 modes that support masked loads.
> +
> +(define_mode_iterator V1248FH_AVX512VLBW
> +  [V16SF (V8SF "TARGET_AVX512VL") (V4SF "TARGET_AVX512VL")
> +   V8DF  (V4DF "TARGET_AVX512VL") (V2DF "TARGET_AVX512VL")
> +   (V32HF "TARGET_AVX512FP16")
> +   (V16HF "TARGET_AVX512FP16 && TARGET_AVX512VL")
> +   (V8HF  "TARGET_AVX512FP16 && TARGET_AVX512VL")
> +   V16SI (V8SI "TARGET_AVX512VL") (V4SI "TARGET_AVX512VL")
> +   V8DI  (V4DI "TARGET_AVX512VL") (V2DI "TARGET_AVX512VL")
> +   (V64QI "TARGET_AVX512BW")
> +   (V32QI "TARGET_AVX512VL && TARGET_AVX512BW")
> +   (V16QI "TARGET_AVX512VL && TARGET_AVX512BW")
> +   (V32HI "TARGET_AVX512BW")
> +   (V16HI "TARGET_AVX512VL && TARGET_AVX512BW")
> +   (V8HI  "TARGET_AVX512VL && TARGET_AVX512BW")])
> +
> +;; Fold masked load into commutative op, no outer mask.  Only ops
> +;; where 0 op x == 0 (mult, and, umin) are safe here: the masked-off
> +;; lanes of the load are zero, and forcing the result lanes to zero
> +;; matches the op only for these.  Other ops need the outer-mask form.
> +(define_insn_and_split "*comm_maskload_fold_unmasked_op"
> +  [(set (match_operand:V1248FH_AVX512VLBW 0 "register_operand")
> +     (match_operator:V1248FH_AVX512VLBW 1 "zero_absorbing_operator"

0 * x == 0 doesn't hold for floating point: 0 * -1.0 is -0.0, and
0 * Inf and 0 * NaN are NaN.  With the patch

  __m512d
  f (const double *p, __mmask8 k, __m512d x)
  {
    return _mm512_mul_pd (_mm512_maskz_loadu_pd (k, p), x);
  }

becomes vmulpd (%rdi), %zmm0, %zmm0{%k1}{z}, and the masked-off lanes
are +0.0 instead of -0.0/NaN.  The FP case is only valid when
!HONOR_NANS && !HONOR_SIGNED_ZEROS, i.e. the -Ofast case in the PR.

> +       [(vec_merge:V1248FH_AVX512VLBW
> +          (unspec:V1248FH_AVX512VLBW
> +             [(match_operand:V1248FH_AVX512VLBW 2 "memory_operand")]
> +             UNSPEC_MASKLOAD)
> +          (match_operand:V1248FH_AVX512VLBW 3 "const0_operand")
> +          (match_operand:<avx512fmaskmode> 4 "register_operand"))
> +        (match_operand:V1248FH_AVX512VLBW 5 "register_operand")]))]
> +  "TARGET_AVX512F"

These have no constraints, so they should only match before reload:
TARGET_AVX512F && ix86_pre_reload_split ().

> +  "#"
> +  "&& 1"
> +  [(const_int 0)]
> +{
> +  rtx op = gen_rtx_fmt_ee (GET_CODE (operands[1]), <MODE>mode,
> +                        operands[5], operands[2]);
> +  rtx vm = gen_rtx_VEC_MERGE (<MODE>mode, op, operands[3], operands[4]);
> +  emit_insn (gen_rtx_SET (operands[0], vm));
> +  DONE;
> +})
> +
> +;; Fold masked load into commutative op, outer mask.
> +(define_insn_and_split "*comm_maskload_fold_masked_op"
> +  [(set (match_operand:V1248FH_AVX512VLBW 0 "register_operand")
> +     (vec_merge:V1248FH_AVX512VLBW
> +       (match_operator:V1248FH_AVX512VLBW 1 "commutative_operator"
> +         [(vec_merge:V1248FH_AVX512VLBW
> +             (unspec:V1248FH_AVX512VLBW

Not every (code, mode) pair here has a masked insn to split into.
There is no masked vpand/vpor/vpxor for byte/word elements, so

  void
  f (char *__restrict d, const char *__restrict a,
     const char *__restrict b, int n)
  {
    for (int i = 0; i < n; i++)
      d[i] = a[i] & b[i];
  }

ICEs in split1 with "unrecognizable insn" at -O3 -march=x86-64-v4
--param vect-partial-vector-usage=1, and so does the short version.
pr67480.c, pr93094.c, pr126982.c and shift-gf2p8affine-2.c all ICE
this way with those options.

> +               [(match_operand:V1248FH_AVX512VLBW 2 "memory_operand")]
> +               UNSPEC_MASKLOAD)
> +             (match_operand:V1248FH_AVX512VLBW 3 "const0_operand")
> +             (match_operand:<avx512fmaskmode> 4 "register_operand"))
> +          (match_operand:V1248FH_AVX512VLBW 5 "register_operand")])
> +       (match_operand:V1248FH_AVX512VLBW 6 "const0_operand")
> +       (match_dup 4)))]
> +  "TARGET_AVX512F"
> +  "#"
> +  "&& 1"
> +  [(const_int 0)]
> +{
> +  rtx op = gen_rtx_fmt_ee (GET_CODE (operands[1]), <MODE>mode,
> +                        operands[5], operands[2]);
> +  rtx vm = gen_rtx_VEC_MERGE (<MODE>mode, op, operands[6], operands[4]);
> +  emit_insn (gen_rtx_SET (operands[0], vm));
> +  DONE;
> +})
> +
> +;; Fold masked load into non-commutative op, outer mask.
> +(define_insn_and_split "*noncomm_maskload_fold_masked_op"
> +  [(set (match_operand:V1248FH_AVX512VLBW 0 "register_operand")
> +     (vec_merge:V1248FH_AVX512VLBW
> +       (match_operator:V1248FH_AVX512VLBW 1 "sub_div_operator"
> +         [(match_operand:V1248FH_AVX512VLBW 5 "register_operand")
> +          (vec_merge:V1248FH_AVX512VLBW
> +             (unspec:V1248FH_AVX512VLBW
> +               [(match_operand:V1248FH_AVX512VLBW 2 "memory_operand")]
> +               UNSPEC_MASKLOAD)
> +             (match_operand:V1248FH_AVX512VLBW 3 "const0_operand")
> +             (match_operand:<avx512fmaskmode> 4 "register_operand"))])
> +       (match_operand:V1248FH_AVX512VLBW 6 "const0_operand")
> +       (match_dup 4)))]
> +  "TARGET_AVX512F"
> +  "#"
> +  "&& 1"
> +  [(const_int 0)]
> +{
> +  rtx op = gen_rtx_fmt_ee (GET_CODE (operands[1]), <MODE>mode,
> +                        operands[5], operands[2]);
> +  rtx vm = gen_rtx_VEC_MERGE (<MODE>mode, op, operands[6], operands[4]);
> +  emit_insn (gen_rtx_SET (operands[0], vm));
> +  DONE;
> +})
> +
> +
>  ;; Standard scalar operation patterns which preserve the rest of the
>  ;; vector for combiner.
>  (define_insn "*<sse>_vm<multdiv_mnemonic><mode>3"
> diff --git a/gcc/testsuite/gcc.target/i386/avx512-maskload-fold-1.c 
> b/gcc/testsuite/gcc.target/i386/avx512-maskload-fold-1.c
> new file mode 100644
> index 00000000000..9024eba79b5
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/i386/avx512-maskload-fold-1.c
> @@ -0,0 +1,32 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O3 -march=x86-64-v4 --param vect-partial-vector-usage=1" } 
> */
> +
> +/* { dg-final { scan-assembler-times "vaddpd\[ 
> \\t\]+\[^\n\]*\\(\[^\n\]*%zmm\[0-9\]+\{%k\[1-7\]\}\{z\}" 1 } } */
> +/* { dg-final { scan-assembler-times "vmulpd\[ 
> \\t\]+\[^\n\]*\\(\[^\n\]*%zmm\[0-9\]+\{%k\[1-7\]\}\{z\}" 1 } } */
> +/* { dg-final { scan-assembler-times "vpaddd\[ 
> \\t\]+\[^\n\]*\\(\[^\n\]*%zmm\[0-9\]+\{%k\[1-7\]\}\{z\}" 1 } } */
> +/* { dg-final { scan-assembler-times "vpmulld\[ 
> \\t\]+\[^\n\]*\\(\[^\n\]*%zmm\[0-9\]+\{%k\[1-7\]\}\{z\}" 1 } } */
> +/* { dg-final { scan-assembler-times "vsubps\[ 
> \\t\]+\[^\n\]*\\(\[^\n\]*%zmm\[0-9\]+\{%k\[1-7\]\}\{z\}" 1 } } */
> +/* { dg-final { scan-assembler-times "vpsubq\[ 
> \\t\]+\[^\n\]*\\(\[^\n\]*%zmm\[0-9\]+\{%k\[1-7\]\}\{z\}" 1 } } */
> +
> +void vector_add_fp (double *__restrict dst, const double *__restrict a,
> +                    const double *__restrict b, int n)
> +{ for (int i = 0; i < n; ++i) dst[i] = a[i] + b[i]; }
> +
> +void vector_mul_fp (double *__restrict dst, const double *__restrict a,
> +                    const double *__restrict b, int n)
> +{ for (int i = 0; i < n; ++i) dst[i] = a[i] * b[i]; }
> +
> +void vector_add_int (int *__restrict dst, const int *__restrict a,
> +                     const int *__restrict b, int n)
> +{ for (int i = 0; i < n; ++i) dst[i] = a[i] + b[i]; }
> +
> +void vector_mul_int (int *__restrict dst, const int *__restrict a,
> +                     const int *__restrict b, int n)
> +{ for (int i = 0; i < n; ++i) dst[i] = a[i] * b[i]; }
> +
> +void vector_sub_fp (float *__restrict dst, const float *__restrict a,
> +                    const float *__restrict b, int n)
> +{ for (int i = 0; i < n; ++i) dst[i] = a[i] - b[i]; }
> +
> +void vector_sub_int64 (long long *__restrict dst, const long long 
> *__restrict a,
> +                       const long long *__restrict b, int n)
> +{ for (int i = 0; i < n; ++i) dst[i] = a[i] - b[i]; }
> diff --git a/gcc/testsuite/gcc.target/i386/avx512-maskload-fold-2.c 
> b/gcc/testsuite/gcc.target/i386/avx512-maskload-fold-2.c
> new file mode 100644
> index 00000000000..6d69cc646d4
> --- /dev/null
> +++ b/gcc/testsuite/gcc.target/i386/avx512-maskload-fold-2.c
> @@ -0,0 +1,8 @@
> +/* { dg-do compile } */
> +/* { dg-options "-O3 -march=x86-64-v4 --param vect-partial-vector-usage=1" } 
> */
> +
> +/* { dg-final { scan-assembler-not "vsubps\[ 
> \\t\]+\[^\n\]*\\(\[^\n\]*%zmm\[0-9\]+\{%k\[1-7\]\}\{z\}" } } */
> +
> +void vector_sub_no_fold (float *__restrict dst, const float *__restrict a,
> +                         float b, int n)
> +{ for (int i = 0; i < n; i++) dst[i] = a[i] - b; }
> -- 
> 2.34.1

Reply via email to