From: "Hu, Lin1" <lin1.hu@intel.com>
To: Tamar Christina <Tamar.Christina@arm.com>,
Richard Biener <rguenther@suse.de>
Cc: "gcc-patches@gcc.gnu.org" <gcc-patches@gcc.gnu.org>,
"Liu, Hongtao" <hongtao.liu@intel.com>,
"ubizjak@gmail.com" <ubizjak@gmail.com>
Subject: RE: [PATCH 1/3 v3] vect: generate suitable convert insn for int -> int, float -> float and int <-> float.
Date: Tue, 25 Jun 2024 02:00:14 +0000 [thread overview]
Message-ID: <SJ0PR11MB5940D39DDA3C6B8C97CF9A33A6D52@SJ0PR11MB5940.namprd11.prod.outlook.com> (raw)
In-Reply-To: <VI1PR08MB5325581136A38C1AAB959DAFFFD42@VI1PR08MB5325.eurprd08.prod.outlook.com>
> -----Original Message-----
> From: Tamar Christina <Tamar.Christina@arm.com>
> Sent: Monday, June 24, 2024 10:12 PM
> To: Richard Biener <rguenther@suse.de>; Hu, Lin1 <lin1.hu@intel.com>
> Cc: gcc-patches@gcc.gnu.org; Liu, Hongtao <hongtao.liu@intel.com>;
> ubizjak@gmail.com
> Subject: RE: [PATCH 1/3 v3] vect: generate suitable convert insn for int -> int,
> float -> float and int <-> float.
>
> > -----Original Message-----
> > From: Richard Biener <rguenther@suse.de>
> > Sent: Monday, June 24, 2024 1:34 PM
> > To: Hu, Lin1 <lin1.hu@intel.com>
> > Cc: gcc-patches@gcc.gnu.org; Liu, Hongtao <hongtao.liu@intel.com>;
> > ubizjak@gmail.com
> > Subject: RE: [PATCH 1/3 v3] vect: generate suitable convert insn for
> > int -> int, float
> > -> float and int <-> float.
> >
> > On Thu, 20 Jun 2024, Hu, Lin1 wrote:
> >
> > > > > else if (ret_elt_bits > arg_elt_bits)
> > > > > modifier = WIDEN;
> > > > >
> > > > > + if (supportable_convert_operation (code, ret_type, arg_type, &code1))
> > > > > + {
> > > > > + g = gimple_build_assign (lhs, code1, arg);
> > > > > + gsi_replace (gsi, g, false);
> > > > > + return;
> > > > > + }
> > > >
> > > > Given the API change I suggest below it might make sense to have
> > > > supportable_indirect_convert_operation do the above and represent
> > > > it as
> > single-
> > > > step conversion?
> > > >
> > >
> > > OK, if you want to supportable_indirect_convert_operation can do
> > > something like supportable_convert_operation, I'll give it a try.
> > > This functionality is really the part that this function can cover.
> > > But this would require some changes not only the API change, because
> > > supportable_indirect_convert_operation originally only supported
> > > Float
> > > -> Int or Int ->Float.
> >
> > I think I'd like to see a single API to handle direct and
> > (multi-)indirect-level converts that operate on vectors with all the
> > same number of lanes.
> >
> > > >
> > > > > + code_helper code2 = ERROR_MARK, code3 = ERROR_MARK;
> > > > > + int multi_step_cvt = 0;
> > > > > + vec<tree> interm_types = vNULL;
> > > > > + if (supportable_indirect_convert_operation (NULL,
> > > > > + code,
> > > > > + ret_type, arg_type,
> > > > > + &code2, &code3,
> > > > > + &multi_step_cvt,
> > > > > + &interm_types, arg))
> > > > > + {
> > > > > + new_rhs = make_ssa_name (interm_types[0]);
> > > > > + g = gimple_build_assign (new_rhs, (tree_code) code3, arg);
> > > > > + gsi_insert_before (gsi, g, GSI_SAME_STMT);
> > > > > + g = gimple_build_assign (lhs, (tree_code) code2, new_rhs);
> > > > > + gsi_replace (gsi, g, false);
> > > > > + return;
> > > > > + }
> > > > > +
> > > > > if (modifier == NONE && (code == FIX_TRUNC_EXPR || code ==
> > > > FLOAT_EXPR))
> > > > > {
> > > > > - if (supportable_convert_operation (code, ret_type, arg_type,
> &code1))
> > > > > - {
> > > > > - g = gimple_build_assign (lhs, code1, arg);
> > > > > - gsi_replace (gsi, g, false);
> > > > > - return;
> > > > > - }
> > > > > /* Can't use get_compute_type here, as
> supportable_convert_operation
> > > > > doesn't necessarily use an optab and needs two arguments. */
> > > > > tree vec_compute_type
> > > > > diff --git a/gcc/tree-vect-stmts.cc b/gcc/tree-vect-stmts.cc
> > > > > index 05a169ecb2d..0aa608202ca 100644
> > > > > --- a/gcc/tree-vect-stmts.cc
> > > > > +++ b/gcc/tree-vect-stmts.cc
> > > > > @@ -5175,7 +5175,7 @@ vectorizable_conversion (vec_info *vinfo,
> > > > > tree scalar_dest;
> > > > > tree op0, op1 = NULL_TREE;
> > > > > loop_vec_info loop_vinfo = dyn_cast <loop_vec_info> (vinfo);
> > > > > - tree_code tc1, tc2;
> > > > > + tree_code tc1;
> > > > > code_helper code, code1, code2;
> > > > > code_helper codecvt1 = ERROR_MARK, codecvt2 = ERROR_MARK;
> > > > > tree new_temp;
> > > > > @@ -5384,92 +5384,17 @@ vectorizable_conversion (vec_info *vinfo,
> > > > > break;
> > > > > }
> > > > >
> > > > > - /* For conversions between float and integer types try whether
> > > > > - we can use intermediate signed integer types to support the
> > > > > - conversion. */
> > > > > - if (GET_MODE_SIZE (lhs_mode) != GET_MODE_SIZE (rhs_mode)
> > > > > - && (code == FLOAT_EXPR ||
> > > > > - (code == FIX_TRUNC_EXPR && !flag_trapping_math)))
> > > > > - {
> > > > > - bool demotion = GET_MODE_SIZE (rhs_mode) > GET_MODE_SIZE
> > > > (lhs_mode);
> > > > > - bool float_expr_p = code == FLOAT_EXPR;
> > > > > - unsigned short target_size;
> > > > > - scalar_mode intermediate_mode;
> > > > > - if (demotion)
> > > > > - {
> > > > > - intermediate_mode = lhs_mode;
> > > > > - target_size = GET_MODE_SIZE (rhs_mode);
> > > > > - }
> > > > > - else
> > > > > - {
> > > > > - target_size = GET_MODE_SIZE (lhs_mode);
> > > > > - if (!int_mode_for_size
> > > > > - (GET_MODE_BITSIZE (rhs_mode), 0).exists
> > > > (&intermediate_mode))
> > > > > - goto unsupported;
> > > > > - }
> > > > > - code1 = float_expr_p ? code : NOP_EXPR;
> > > > > - codecvt1 = float_expr_p ? NOP_EXPR : code;
> > > > > - opt_scalar_mode mode_iter;
> > > > > - FOR_EACH_2XWIDER_MODE (mode_iter, intermediate_mode)
> > > > > - {
> > > > > - intermediate_mode = mode_iter.require ();
> > > > > -
> > > > > - if (GET_MODE_SIZE (intermediate_mode) > target_size)
> > > > > - break;
> > > > > -
> > > > > - scalar_mode cvt_mode;
> > > > > - if (!int_mode_for_size
> > > > > - (GET_MODE_BITSIZE (intermediate_mode), 0).exists
> > > > (&cvt_mode))
> > > > > - break;
> > > > > -
> > > > > - cvt_type = build_nonstandard_integer_type
> > > > > - (GET_MODE_BITSIZE (cvt_mode), 0);
> > > > > -
> > > > > - /* Check if the intermediate type can hold OP0's range.
> > > > > - When converting from float to integer this is not necessary
> > > > > - because values that do not fit the (smaller) target type are
> > > > > - unspecified anyway. */
> > > > > - if (demotion && float_expr_p)
> > > > > - {
> > > > > - wide_int op_min_value, op_max_value;
> > > > > - if (!vect_get_range_info (op0, &op_min_value,
> > > > &op_max_value))
> > > > > - break;
> > > > > -
> > > > > - if (cvt_type == NULL_TREE
> > > > > - || (wi::min_precision (op_max_value, SIGNED)
> > > > > - > TYPE_PRECISION (cvt_type))
> > > > > - || (wi::min_precision (op_min_value, SIGNED)
> > > > > - > TYPE_PRECISION (cvt_type)))
> > > > > - continue;
> > > > > - }
> > > > > -
> > > > > - cvt_type = get_vectype_for_scalar_type (vinfo, cvt_type, slp_node);
> > > > > - /* This should only happened for SLP as long as loop vectorizer
> > > > > - only supports same-sized vector. */
> > > > > - if (cvt_type == NULL_TREE
> > > > > - || maybe_ne (TYPE_VECTOR_SUBPARTS (cvt_type), nunits_in)
> > > > > - || !supportable_convert_operation ((tree_code) code1,
> > > > > - vectype_out,
> > > > > - cvt_type, &tc1)
> > > > > - || !supportable_convert_operation ((tree_code) codecvt1,
> > > > > - cvt_type,
> > > > > - vectype_in, &tc2))
> > > > > - continue;
> > > > > -
> > > > > - found_mode = true;
> > > > > - break;
> > > > > - }
> > > > > + if (supportable_indirect_convert_operation (vinfo,
> > > > > + code,
> > > > > + vectype_out,
> > > > > + vectype_in,
> > > > > + &code1,
> > > > > + &codecvt1,
> > > > > + &multi_step_cvt,
> > > > > + &interm_types,
> > > > > + op0,slp_node))
> > > > > + break;
> > > > >
> > > > > - if (found_mode)
> > > > > - {
> > > > > - multi_step_cvt++;
> > > > > - interm_types.safe_push (cvt_type);
> > > > > - cvt_type = NULL_TREE;
> > > > > - code1 = tc1;
> > > > > - codecvt1 = tc2;
> > > > > - break;
> > > > > - }
> > > > > - }
> > > > > /* FALLTHRU */
> > > > > unsupported:
> > > > > if (dump_enabled_p ())
> > > > > @@ -14626,6 +14551,153 @@ supportable_narrowing_operation
> > > > (code_helper code,
> > > > > return false;
> > > > > }
> > > > >
> > > > > +/* Function supportable_indirect_convert_operation
> > > > > +
> > > > > + Check whether an operation represented by the code CODE is two
> > > > > + convert operations that are supported by the target platform in
> > > > > + vector form (i.e., when operating on arguments of type VECTYPE_IN
> > > > > + producing a result of type VECTYPE_OUT).
> > > > > +
> > > > > + Convert operations we currently support directly are
> > > > > + FIX_TRUNC and
> > FLOAT.
> > > > > + This function checks if these operations are supported
> > > > > + by the target platform directly (via vector tree-codes).
> > > > > +
> > > > > + Output:
> > > > > + - CODE1 is the code of a vector operation to be used when
> > > > > + converting the operation in the first step, if available.
> > > > > + - CODE2 is the code of a vector operation to be used when
> > > > > + converting the operation in the second step, if available.
> > > > > + - MULTI_STEP_CVT determines the number of required
> > > > > + intermediate steps
> > > > in
> > > > > + case of multi-step conversion (like int->short->char - in that case
> > > > > + MULTI_STEP_CVT will be 1). In the function, it should be 1.
> > > > > + - INTERM_TYPES contains the intermediate type required to perform
> the
> > > > > + convert operation (short in the above example). */
> > > > > +bool
> > > > > +supportable_indirect_convert_operation (vec_info *vinfo,
> > > > > + code_helper code,
> > > > > + tree vectype_out,
> > > > > + tree vectype_in,
> > > > > + code_helper *code1,
> > > > > + code_helper *code2,
> > > > > + int *multi_step_cvt,
> > > > > + vec<tree> *interm_types,
> > > >
> > > > This API is somewhat awkward, as we're inventing a new one I guess
> > > > we can do better. I think we want
> > > >
> > > > vec<std::pair<tree, tree_code> > *converts,
> > > >
> > > > covering all code1, code2, multi_step_cvt and interm_types with
> > > > the
> > conversion
> > > > sequence being
> > > >
> > > > converts[0].first tem0 = converts[0].second op0;
> > > > converts[1].first tem1 = converts[1].second tem;
> > > >
> > >
> > > That's great, this really makes the function work better.
> > >
> > > >
> > > > ... while converts.length () determines the length of the chain,
> > > > one being a
> > direct
> > > > conversion where then converts[0].first is vectype_out. That
> > > > would allow double -> char to go double -> float -> int -> short -> char for
> example.
> > > >
> > >
> > > I'm trying to determine the requirements, do you want this function
> > > to support multiple conversions (the current implementation just
> > > does a two-step conversion, like double -> char, which becomes
> > > double -> int -> char). Actually we should be able to do all
> > > conversions in two steps, if we have some suitable instructions. I
> > > can't think of a scenario where multiple conversions are needed yet.
> > > Could you give me some examples? Of course, I could tweak this
> > > feature in advance if it is for future consideration.
> >
> > I think the API should support multi-level, not only two levels. The
> > implementation doesn't need to cover that case unless we run into such
> > a requirement. Usually vector ISAs allow 2x integer
> > widening/shortening but not 4x, so a VnDImode -> VnQImode conversion
> > would need to go via VnSImode and VnHImode (of course some targets
> > might "help" the vectorizer by providing a VnDImode -> VnQImode
> > pattern that does the intermediate conversions behind the vectorizers
> > back). But yes, the original motivation for the vectorizer code was
> > that float<->int conversions are limited.
> >
>
> I have a similar patch in this area but instead looking at unsigned int <-> double
> conversion. I would want to avoid complicating this area too much so it would
> be good if the API doesn't care about sign either and allows the target to choose
> the operation mode?
>
> My current patch has a backend target hook that asks if the current widening is
> Preferred as multilevel or single level. For single level I just generate
> VEC_PERM_EXPRs with a zero register.
>
> Just wanted to bring it up in case we can have a coherent story around this
> conversions.
>
This is the current API.
14579 bool
14580 supportable_indirect_convert_operation (code_helper code,
14581 tree vectype_out,
14582 tree vectype_in,
14583 vec<std::pair<tree, tree_code> > *converts,
14584 tree op0)
This API doesn't care about sign, whether double to char or unsigned char will be converted to int first in my opinion.
About the backend target hook, I guess you use targetm.* to determine the target to choose the operation mode, if yes, I think the API is ok for now, you can use targetm to determine the operation mode and output through converts. What do you think?
BRs,
Lin
>
> >
> > >
> > > >
> > > > > + tree op0,
> > > > > + slp_tree slp_node)
> > > >
> > > > I would like to avoid passing VINFO and SLP_NODE here, see below.
> > > > The same is true for OP0 where the existing use is wrong for SLP
> > > > already, but I guess that can stay for now (I opened PR115538 about the
> wrong-code issue).
> > > >
> > >
> > > Thanks, I have removed them.
> > >
> > > >
> > > > > +{
> > > > > + bool found_mode = false;
> > > > > + scalar_mode lhs_mode = GET_MODE_INNER (TYPE_MODE
> > > > > +(vectype_out));
> > > > > + scalar_mode rhs_mode = GET_MODE_INNER (TYPE_MODE
> > > > > +(vectype_in));
> > > > > + opt_scalar_mode mode_iter;
> > > > > + tree_code tc1, tc2;
> > > > > +
> > > > > + tree cvt_type = NULL_TREE;
> > > > > + poly_uint64 nelts = TYPE_VECTOR_SUBPARTS (vectype_in);
> > > > > +
> > > > > + (*multi_step_cvt) = 0;
> > > > > + /* For conversions between float and integer types try whether
> > > > > + we can use intermediate signed integer types to support the
> > > > > + conversion. */
> > > > > + if (GET_MODE_SIZE (lhs_mode) != GET_MODE_SIZE (rhs_mode)
> > > > > + && (code == FLOAT_EXPR
> > > > > + || (code == FIX_TRUNC_EXPR && !flag_trapping_math)))
> > > > > + {
> > > > > + bool demotion = GET_MODE_SIZE (rhs_mode) > GET_MODE_SIZE
> > > > (lhs_mode);
> > > > > + bool float_expr_p = code == FLOAT_EXPR;
> > > > > + unsigned short target_size;
> > > > > + scalar_mode intermediate_mode;
> > > > > + if (demotion)
> > > > > + {
> > > > > + intermediate_mode = lhs_mode;
> > > > > + target_size = GET_MODE_SIZE (rhs_mode);
> > > > > + }
> > > > > + else
> > > > > + {
> > > > > + target_size = GET_MODE_SIZE (lhs_mode);
> > > > > + if (!int_mode_for_size
> > > > > + (GET_MODE_BITSIZE (rhs_mode), 0).exists
> (&intermediate_mode))
> > > > > + return false;
> > > > > + }
> > > > > + *code1 = float_expr_p ? code : NOP_EXPR;
> > > > > + *code2 = float_expr_p ? NOP_EXPR : code;
> > > > > + opt_scalar_mode mode_iter;
> > > > > + FOR_EACH_2XWIDER_MODE (mode_iter, intermediate_mode)
> > > > > + {
> > > > > + intermediate_mode = mode_iter.require ();
> > > > > +
> > > > > + if (GET_MODE_SIZE (intermediate_mode) > target_size)
> > > > > + break;
> > > > > +
> > > > > + scalar_mode cvt_mode;
> > > > > + if (!int_mode_for_size
> > > > > + (GET_MODE_BITSIZE (intermediate_mode), 0).exists
> (&cvt_mode))
> > > > > + break;
> > > > > +
> > > > > + cvt_type = build_nonstandard_integer_type
> > > > > + (GET_MODE_BITSIZE (cvt_mode), 0);
> > > > > +
> > > > > + /* Check if the intermediate type can hold OP0's range.
> > > > > + When converting from float to integer this is not necessary
> > > > > + because values that do not fit the (smaller) target type are
> > > > > + unspecified anyway. */
> > > > > + if (demotion && float_expr_p)
> > > > > + {
> > > > > + wide_int op_min_value, op_max_value;
> > > > > + /* For vector form, it looks like op0 doesn't have
> RANGE_INFO.
> > > > > + In the future, if it is supported, changes may need to be
> made
> > > > > + to this part, such as checking the RANGE of each
> element
> > > > > + in the vector. */
> > > > > + if (!SSA_NAME_RANGE_INFO (op0)
> > > > > + || !vect_get_range_info (op0, &op_min_value,
> > > > &op_max_value))
> > > > > + break;
> > > > > +
> > > > > + if (cvt_type == NULL_TREE
> > > > > + || (wi::min_precision (op_max_value, SIGNED)
> > > > > + > TYPE_PRECISION (cvt_type))
> > > > > + || (wi::min_precision (op_min_value, SIGNED)
> > > > > + > TYPE_PRECISION (cvt_type)))
> > > > > + continue;
> > > > > + }
> > > > > +
> > > > > + if (vinfo != NULL && slp_node != NULL)
> > > > > + cvt_type = get_vectype_for_scalar_type (vinfo, cvt_type,
> slp_node);
> > > > > + else
> > > > > + {
> > > > > + bool uns = TYPE_UNSIGNED (TREE_TYPE (vectype_out))
> > > > > + || TYPE_UNSIGNED (TREE_TYPE (vectype_in));
> > > > > + cvt_type = build_nonstandard_integer_type
> > > > > + (GET_MODE_BITSIZE (cvt_mode), uns);
> > > > > + cvt_type = build_vector_type (cvt_type, nelts);
> > > > > + }
> > > >
> > > > So this would then become
> > > >
> > > > cvt_type = get_related_vectype_for_scalar_type
> > > > (TYPE_MODE (vectype_in), cvt_type, TYPE_VECTOR_SUBPARTS
> > > > (vectype_in));
> > > >
> > > > > + /* This should only happened for SLP as long as loop vectorizer
> > > > > + only supports same-sized vector. */
> > > > > + if (cvt_type == NULL_TREE
> > > > > + || maybe_ne (TYPE_VECTOR_SUBPARTS (cvt_type), nelts)
> > > > > + || !supportable_convert_operation ((tree_code) *code1,
> > > > > + vectype_out,
> > > > > + cvt_type, &tc1)
> > > > > + || !supportable_convert_operation ((tree_code) *code2,
> > > > > + cvt_type,
> > > > > + vectype_in, &tc2))
> > > > > + continue;
> > > > > +
> > > > > + found_mode = true;
> > > > > + break;
> > > > > + }
> > > > > +
> > > > > + if (found_mode)
> > > > > + {
> > > > > + (*multi_step_cvt)++;
> > > > > + interm_types->safe_push (cvt_type);
> > > > > + cvt_type = NULL_TREE;
> > > > > + *code1 = tc1;
> > > > > + *code2 = tc2;
> > > > > + return true;
> > > > > + }
> > > > > + }
> > > > > + interm_types->release ();
> > > >
> > > > Hmm, ownership of interm_types is somewhat unclear here - the
> > > > caller should release it, or is the situation that the caller is confused by
> stray elements in it?
> > In
> > > > that case I'd suggest to instead do interm_types->truncate (0).
> > > >
> > >
> > > It's my fault, I just imitate
> > > supportable_narrowing/widening_operation,
> > > I think for this function, interm_types->release() is not needed.
next prev parent reply other threads:[~2024-06-25 2:00 UTC|newest]
Thread overview: 33+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-05-08 1:38 [PATCH] " Hu, Lin1
2024-05-14 2:25 ` Hu, Lin1
2024-05-14 12:23 ` Richard Biener
2024-05-15 2:30 ` Hu, Lin1
2024-05-23 6:37 ` [PATCH 0/3] Optimize __builtin_convertvector for x86-64-v4 and Hu, Lin1
2024-05-23 6:37 ` [PATCH 1/3] vect: generate suitable convert insn for int -> int, float -> float and int <-> float Hu, Lin1
2024-05-29 9:40 ` Richard Biener
2024-05-31 8:54 ` Hu, Lin1
2024-05-31 12:41 ` Richard Biener
2024-06-03 8:23 ` Hu, Lin1
2024-06-03 9:02 ` Richard Biener
2024-06-03 9:26 ` Hu, Lin1
2024-06-03 9:30 ` Richard Biener
2024-06-11 6:49 ` [PATCH 1/3 v3] " Hu, Lin1
2024-06-17 1:48 ` Hu, Lin1
2024-06-18 11:44 ` Richard Biener
2024-06-20 11:26 ` Hu, Lin1
2024-06-24 12:33 ` Richard Biener
2024-06-24 14:12 ` Tamar Christina
2024-06-25 2:00 ` Hu, Lin1 [this message]
2024-06-25 3:28 ` [PATCH 1/3 v4] " Hu, Lin1
2024-06-25 13:30 ` Richard Biener
2024-06-26 10:54 ` [PATCH 1/3 v5] " Hu, Lin1
2024-05-23 6:37 ` [PATCH 2/3] vect: Support v4hi -> v4qi Hu, Lin1
2024-05-27 2:11 ` Hongtao Liu
2024-05-29 8:55 ` [PATCH 2/3 v2] " Hu, Lin1
2024-05-29 9:09 ` Hongtao Liu
2024-05-23 6:37 ` [PATCH 3/3] vect: support direct conversion under x86-64-v3 Hu, Lin1
2024-05-23 6:42 ` Hongtao Liu
2024-05-23 7:17 ` Hu, Lin1
2024-05-23 8:05 ` Hongtao Liu
2024-05-29 8:59 ` [PATCH 3/3 v2] " Hu, Lin1
2024-05-30 6:51 ` Hongtao Liu
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=SJ0PR11MB5940D39DDA3C6B8C97CF9A33A6D52@SJ0PR11MB5940.namprd11.prod.outlook.com \
--to=lin1.hu@intel.com \
--cc=Tamar.Christina@arm.com \
--cc=gcc-patches@gcc.gnu.org \
--cc=hongtao.liu@intel.com \
--cc=rguenther@suse.de \
--cc=ubizjak@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).