Tarantool development patches archive
 help / color / mirror / Atom feed
From: Sergey Kaplun via Tarantool-patches <tarantool-patches@dev.tarantool.org>
To: Maxim Kokryashkin <max.kokryashkin@gmail.com>
Cc: tarantool-patches@dev.tarantool.org
Subject: Re: [Tarantool-patches] [PATCH luajit v6] Fix math.min()/math.max() inconsistencies.
Date: Fri, 23 Dec 2022 18:42:49 +0300	[thread overview]
Message-ID: <Y6XMebt03ffqqDPQ@root> (raw)
In-Reply-To: <20221219095228.126312-1-m.kokryashkin@tarantool.org>

Hi Maxim!

Thanks for the fixes!

Please consider my comments below.

Also, during a flash of inspiration and debugging test cases for this
particular patch I found the test case for `math.modf()`. So, we need
an additional patch with the test as a followup to the backported
commit [8].

The following code:
| LUA_PATH="src/?.lua;;" src/luajit -Ohotloop=1 -e '
| local modf = math.modf
| local nan = 0/0
| local inf = math.huge
|
| local r1 = {nil, nil, nil, nil}
| local r2 = {nil, nil, nil, nil}
| local r3 = {nil, nil, nil, nil}
| local r4 = {nil, nil, nil, nil}
| print("MODF")
| for i = 1, 4 do
|   r1[i], r2[i] = modf(inf)
|   r3[i], r4[i] = modf(nan)
| end
| print(r1[1],  r1[2],  r1[3],  r1[4])
| print(r2[1],  r2[2],  r2[3],  r2[4])
| print("")
| print(r3[1],  r3[2],  r3[3],  r3[4])
| print(r4[1],  r4[2],  r4[3],  r4[4])
| '

Returns before the patch (on both arches):

| MODF
| inf     inf     inf     inf
| 0       0       nan     nan
|
| nan     nan     nan     nan
| nan     nan     nan     nan

And after patch (on both arches):

| MODF
| inf     inf     inf     inf
| 0       0       0       0
|
| nan     nan     nan     nan
| nan     nan     nan     nan

On 19.12.22, Maxim Kokryashkin wrote:
> From: Mike Pall <mike>
> 
> (cherry-picked from commit 03208c8162af9cc01ca76ee1676ca79e5abe9b60)
> 
> `math.min()`/`math.max()` could produce different results.
> Previously, dirty values on the Lua stack could be
> treated as arguments to `math.min()`/`math.max()`.
> This patch adds check for the number of arguments provided to
> math.min/max, which fixes the issue.
> 
> Also it adds the corresponding test case for
> the mentioned issue and does some refactoring:

OK, this is not a refactoring but adding a consistency between the JIT and the
VM behaviour.

> 1. fcc is changed for min/max functions in ARM
> assembly from LO/HI (lower/upper or unordered) to LE/PL
> (lower/upper, equal or unoredered).
> 
> 2. Several fold optimizations for min/max were removed
> or modified.

See my comments with tests below.

> 
> Resolves tarantool/tarantool#6163
> ---
> 
> >IMHO, LO -> LT (N!=V Less than or unordered) do the same thing wo
> >changing the order.
> >
> >IINM, an ordered comparison checks if neither operand is NaN.
> >Conversely, an unordered comparison checks if either operand is a NaN.
> >
> >So, looks like an attempt to fix inconsistent behaviour for NaNs in
> >math.min/math.max on aarch64.
> >
> >Also, I found inconsistent behaviour on x86 (between LuaJIT|Lua):
> >
> >| # on upstream build
> >| $ ./luajit -Ohotloop=1 -e 'local res = {} for i = 1,4 do res[i] = math.max(0/0, math.huge) end for i = 1, #res do print(res[i]) end'
> >| inf
> >| inf
> >| inf
> >| inf
> >| $ lua -e 'local res = {} for i = 1,4 do res[i] = math.max(0/0, math.huge) end for i = 1, #res do print(res[i]) end'
> >| -nan
> >| -nan
> >| -nan
> >| -nan
> >
> >Can you please test some similar examples on aarch64/M1?
> I've tested those on M1 and here are the results:
> $ ./src/luajit -Ohotloop=1 -e 'local res = {} for i=1,4 do res[i]=math.max(0/0,math.huge) end for i =1, #res do print(res[i]) end'
> inf
> inf
> inf
> inf
> 
> $ lua -e 'local res = {} for i=1,4 do res[i]=math.max(0/0,math.huge) end for i =1, #res do print(res[i]) end'
> nan
> nan
> nan
> nan
> 
> >Also, AFAICS, some optimizations are the reason of inconsistent
> >behaviour for JIT-ed code (not the fold in this commit).
> >| # on our fork
> >| ./luajit -O0 -Ohotloop=1 -e 'local res = {} for i = 1,4 do res[i] = math.max(0/0, math.huge) end for i = 1, #res do print(res[i]) end'
> >| inf
> >| inf
> >| inf
> >| inf
> >| # on our fork
> >| ./luajit -Ohotloop=1 -e 'local res = {} for i = 1,4 do res[i] = math.max(0/0, math.huge) end for i = 1, #res do print(res[i]) end'
> >| inf
> >| inf
> >| inf
> >| nan
> >
> >BTW this commit doesn't fix the problem. Can you please bisect the
> >commit to backport?
> Works perfectly fine on our fork after this patch.
> 

My, bad. PEBCAK:).
The aforementioned testcase really passes after the whole patch.

Side note: __Five minutes later__:
OK, nevermind, still inconsistent on upstream (in some other way):

| $ src/luajit -Ohotloop=1 -e '
| local inf = math.huge
| local nan = inf/inf
| local min = math.min
| local max = math.max
|
| print(nan, inf)
|
| local r = {}
| local r_assoc = {}
| print("MIN:")
| for i = 1, 4 do
|   r[i] = min(nan, inf)
|   r_assoc[i] =  min(min(nan, inf), nan)
| end
| print(r[1], r[2], r[3], r[4])
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4])
|
| print("MAX:")
| for i = 1, 4 do
|   r[i] = max(nan, inf)
|   r_assoc[i] =  max(max(nan, inf), nan)
| end
| print(r[1], r[2], r[3], r[4])
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4])
| '
| nan     inf
| MIN:
| inf     inf     inf     nan
| nan     nan     nan     nan
| MAX:
| inf     inf     nan     nan
| nan     nan     nan     nan

> >> diff --git a/src/lj_opt_fold.c b/src/lj_opt_fold.c
> >> index 276dc040..07a52a4d 100644
> >> --- a/src/lj_opt_fold.c
> >> +++ b/src/lj_opt_fold.c
> >> @@ -1774,8 +1774,6 @@ LJFOLDF(reassoc_intarith_k64)
> >> #endif
> >> }
> >>
> >> -LJFOLD(MIN MIN any)
> >> -LJFOLD(MAX MAX any)
> >> LJFOLD(BAND BAND any)
> >> LJFOLD(BOR BOR any)
> >> LJFOLDF(reassoc_dup)
> >> @@ -1785,6 +1783,15 @@ LJFOLDF(reassoc_dup)
> >> return NEXTFOLD;
> >> }
> >>
> >> +LJFOLD(MIN MIN any)
> >> +LJFOLD(MAX MAX any)
> >> +LJFOLDF(reassoc_dup_minmax)
> >> +{
> >> + if (fins->op2 == fleft->op2)
> >> + return LEFTFOLD; /* (a o b) o b ==> a o b */
> >> + return NEXTFOLD;
> >> +}
> >> +

I don't understand why this should be fixed by removing this
optimizations (this is about `min(min(a, b), b)` and `max(max(a, b), b)`
constructions).
Do you mean `fold_comm_dup_minmax()` part?

> >
> >Do you know why the opt `(a o b) o a ==> a o b;` is ommited now?
> >Are there any examples of incorrect behaviour? I suggest to check NaN
> >behaviour in this case.
> Added the test case for that one, but I failed to find any for the
> other.
> 
> I've done test runs on all of the combinations of `{1, -1, 0, -0, 0/0,
> -math.huge, math.huge}` for all of the optimizations.
> 
> I'll be glad to add another test case if you can think of any.

OK, I mark all spots with tests to be added as (T). Please, provide
verbose description for each of my comments in commit message and
comments in tests.

> 
>  src/lj_asm_arm.h                              |  6 +--
>  src/lj_asm_arm64.h                            |  6 +--
>  src/lj_opt_fold.c                             | 53 +++++++------------
>  src/lj_vmmath.c                               |  4 +-
>  src/vm_arm.dasc                               |  4 +-
>  src/vm_arm64.dasc                             |  4 +-
>  src/vm_x64.dasc                               |  2 +-
>  src/vm_x86.dasc                               |  2 +-
>  test/tarantool-tests/gh-6163-min-max.test.lua | 48 +++++++++++++++++
>  9 files changed, 81 insertions(+), 48 deletions(-)
>  create mode 100644 test/tarantool-tests/gh-6163-min-max.test.lua
> 
> diff --git a/src/lj_asm_arm.h b/src/lj_asm_arm.h
> index 8af19eb9..6ae6e2f2 100644
> --- a/src/lj_asm_arm.h
> +++ b/src/lj_asm_arm.h
> @@ -1663,8 +1663,8 @@ static void asm_min_max(ASMState *as, IRIns *ir, int cc, int fcc)
>      asm_intmin_max(as, ir, cc);
>  }
>  
> -#define asm_min(as, ir)		asm_min_max(as, ir, CC_GT, CC_HI)
> -#define asm_max(as, ir)		asm_min_max(as, ir, CC_LT, CC_LO)
> +#define asm_min(as, ir)		asm_min_max(as, ir, CC_GT, CC_PL)
> +#define asm_max(as, ir)		asm_min_max(as, ir, CC_LT, CC_LE)
>  
>  /* -- Comparisons --------------------------------------------------------- */
>  
> @@ -1856,7 +1856,7 @@ static void asm_hiop(ASMState *as, IRIns *ir)
>    } else if ((ir-1)->o == IR_MIN || (ir-1)->o == IR_MAX) {
>      as->curins--;  /* Always skip the loword min/max. */
>      if (uselo || usehi)
> -      asm_sfpmin_max(as, ir-1, (ir-1)->o == IR_MIN ? CC_HI : CC_LO);
> +      asm_sfpmin_max(as, ir-1, (ir-1)->o == IR_MIN ? CC_PL : CC_LE);
>      return;
>  #elif LJ_HASFFI
>    } else if ((ir-1)->o == IR_CONV) {
> diff --git a/src/lj_asm_arm64.h b/src/lj_asm_arm64.h
> index 4aeb51f3..fe197700 100644
> --- a/src/lj_asm_arm64.h
> +++ b/src/lj_asm_arm64.h
> @@ -1592,7 +1592,7 @@ static void asm_fpmin_max(ASMState *as, IRIns *ir, A64CC fcc)
>    Reg dest = (ra_dest(as, ir, RSET_FPR) & 31);
>    Reg right, left = ra_alloc2(as, ir, RSET_FPR);
>    right = ((left >> 8) & 31); left &= 31;
> -  emit_dnm(as, A64I_FCSELd | A64F_CC(fcc), dest, left, right);
> +  emit_dnm(as, A64I_FCSELd | A64F_CC(fcc), dest, right, left);
>    emit_nm(as, A64I_FCMPd, left, right);
>  }
>  
> @@ -1604,8 +1604,8 @@ static void asm_min_max(ASMState *as, IRIns *ir, A64CC cc, A64CC fcc)
>      asm_intmin_max(as, ir, cc);
>  }
>  
> -#define asm_max(as, ir)		asm_min_max(as, ir, CC_GT, CC_HI)
> -#define asm_min(as, ir)		asm_min_max(as, ir, CC_LT, CC_LO)
> +#define asm_min(as, ir)		asm_min_max(as, ir, CC_LT, CC_PL)
> +#define asm_max(as, ir)		asm_min_max(as, ir, CC_GT, CC_LE)
>  
>  /* -- Comparisons --------------------------------------------------------- */
>  
> diff --git a/src/lj_opt_fold.c b/src/lj_opt_fold.c

I test each piece of changes on x86/x64 and aarch64 and here are my
observations:

> index 49f74996..27e489af 100644
> --- a/src/lj_opt_fold.c
> +++ b/src/lj_opt_fold.c
> @@ -1797,8 +1797,6 @@ LJFOLDF(reassoc_intarith_k64)
>  #endif
>  }
>  
> -LJFOLD(MIN MIN any)
> -LJFOLD(MAX MAX any)
>  LJFOLD(BAND BAND any)
>  LJFOLD(BOR BOR any)
>  LJFOLDF(reassoc_dup)
> @@ -1808,6 +1806,15 @@ LJFOLDF(reassoc_dup)
>    return NEXTFOLD;
>  }
>  
> +LJFOLD(MIN MIN any)
> +LJFOLD(MAX MAX any)
> +LJFOLDF(reassoc_dup_minmax)
> +{
> +  if (fins->op2 == fleft->op2)
> +    return LEFTFOLD;  /* (a o b) o b ==> a o b */
> +  return NEXTFOLD;
> +}
> +

Avoiding of `(a o b) o a ==> a o b` optimization "fixes" (if I get the
idea right) the cases like the following on arch64:

| min(min(x, nan), x)
where `x` is a finite number.

(T) For example the following snippet:
| LUA_PATH="src/?.lua;;" src/luajit -Ohotloop=1 -e '
| local nan = 0/0
| local x = 1
| local min = math.min
|
| print(x, nan, inf)
| print("MIN:", x, nan)
| local r = {}
| local r_assoc = {}
| for k = 1, 4 do
|   r[k] = min(x, nan)
|   r_assoc[k] =  min(min(x, nan), x)
| end
| print(r[1], r[2], r[3], r[4])
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4], "\n")
| '

prints the following before the patch:
| 1       nan     inf
| MIN:    1       nan
| nan     nan     nan     nan
| 1       1       1       nan

and after patch:
| 1       nan     inf
| MIN:    1       nan
| nan     nan     nan     nan
| 1       1       1       1

For this code we get the following IRs:

| 0003 x8 >  int SLOAD  #4    T       ; 1
| 0004 d3 >  num SLOAD  #3    T       ; nan
| 0005    >  fun EQ     0002  math.min
| 0006 d2    num CONV   0003  num.int ; 1 -> 1.0
| 0007 d1    num MIN    0006  0004    ; res_0007 = min(1.0, nan) = nan


In case after patch with removed fold optimization we get the additional IR
later:

| 0016 d0    num MIN    0007  0006    ; min(res_0007, 1.0)

with the following mcode:
| fcmp  d2, d3         ; fcmp 1.0, nan
| fcsel d1, d2, d3, cc ; d1 == nan after this instruction (*)
| ...
| fcmp  d1, d2         ; fcmp nan, 1.0
| fcsel d0, d1, d2, cc ; d0 == 1.0 after this instruction (*)

Before the patch only one `fcmp` `fcsel` pair is emitted, so `nan` value
is returned in the destination register.

(*) Why this value is chosen:

According to the `fcmp` documentation:

| The IEEE 754 standard specifies that the result of a comparison is
| precisely one of <, ==, > or unordered. If either or both of the
| operands are NaNs, they are unordered, and all three of (Operand1 <
| Operand2), (Operand1 == Operand2) and (Operand1 > Operand2) are false.
| This case results in the FPSCR flags being set to N=0, Z=0, C=1, and
| V=1.

`cc` (aka `CC_LO` == `CC_CC`) condition means that C flag is 0 [2], that
is false when we are comparing something with NaN.

So, according to the `fcsel` documentation [3]:

| If the condition passes, the first SIMD and FP source register value
| is taken, otherwise the second SIMD and FP source register value is
| taken.

So, the value of the second source register is always chosen.

(TT) When we change the order of arguments things become broken again:

| $ LUA_PATH="src/?.lua;;" src/luajit -Ohotloop=1 -e '
| local inf = math.huge
| local nan = 0/0
|
| local x = 1
| local min = math.min
| local max = math.max
|
| print(x, nan, inf)
| print("MIN:", x, nan)
| local r = {}
| local r_assoc = {}
| for k = 1, 4 do
|   r[k] = min(x, nan)
|   r_assoc[k] =  min(x, min(x, nan))
| end
| print(r[1], r[2], r[3], r[4])
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4], "\n")
| '

Prints the following before the patch (on aarch64):

| 1       nan     inf
| MIN:    1       nan
| nan     nan     nan     nan
| nan     nan     nan     nan

And after this particular chunk of the patch (reassoc_dup_minmax):

| 1       nan     inf
| MIN:    1       nan
| nan     nan     nan     nan
| nan     nan     nan     1

>  LJFOLD(BXOR BXOR any)
>  LJFOLDF(reassoc_bxor)
>  {
> @@ -1846,23 +1853,12 @@ LJFOLDF(reassoc_shift)
>    return NEXTFOLD;
>  }
>  
> -LJFOLD(MIN MIN KNUM)
> -LJFOLD(MAX MAX KNUM)
>  LJFOLD(MIN MIN KINT)
>  LJFOLD(MAX MAX KINT)
>  LJFOLDF(reassoc_minmax_k)
>  {
>    IRIns *irk = IR(fleft->op2);
> -  if (irk->o == IR_KNUM) {
> -    lua_Number a = ir_knum(irk)->n;
> -    lua_Number y = lj_vm_foldarith(a, knumright, fins->o - IR_ADD);
> -    if (a == y)  /* (x o k1) o k2 ==> x o k1, if (k1 o k2) == k1. */
> -      return LEFTFOLD;
> -    PHIBARRIER(fleft);
> -    fins->op1 = fleft->op1;
> -    fins->op2 = (IRRef1)lj_ir_knum(J, y);
> -    return RETRYFOLD;  /* (x o k1) o k2 ==> x o (k1 o k2) */
> -  } else if (irk->o == IR_KINT) {
> +  if (irk->o == IR_KINT) {
>      int32_t a = irk->i;
>      int32_t y = kfold_intop(a, fright->i, fins->o);
>      if (a == y)  /* (x o k1) o k2 ==> x o k1, if (k1 o k2) == k1. */
> @@ -1875,24 +1871,6 @@ LJFOLDF(reassoc_minmax_k)
>    return NEXTFOLD;
>  }

(TTT) This chunk fixes behaviour for constants reassociation.

Run the following chunk:
| LUA_PATH="src/?.lua;;" src/luajit -Ohotloop=1 -e '
| local inf = math.huge
| local nan = 0/0
| local min = math.min
| local max = math.max
|
| local r = {}
| local r_assoc = {}
| print("MIN:")
| local x = 1.2
| for i = 1, 4 do
|   r[i] =  min(min(x, 0/0), 1.3)
|   r_assoc[i] =  min(min(x, 1.3), 0/0)
| end
| print(r[1], r[2], r[3], r[4])
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4])
|
| print("MAX:")
| for i = 1, 4 do
|   r[i] =  max(max(x, 0/0), 1.1)
|   r_assoc[i] =  max(max(x, 1.1), 0/0)
| end
| print(r[1], r[2], r[3], r[4])
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4])
| '

Gives the following results before the particular (reassoc_minmax_k)
patch (on both arches):

| MIN:
| 1.3     1.3     1.3     1.2
| nan     nan     nan     nan
| MAX:
| 1.1     1.1     1.2     1.2
| nan     nan     nan     nan

And after patch (on both arches):
| MIN:
| 1.3     1.3     1.3     1.3
| nan     nan     nan     nan
| MAX:
| 1.1     1.1     1.1     1.1
| nan     nan     nan     nan

>  
> -LJFOLD(MIN MAX any)
> -LJFOLD(MAX MIN any)
> -LJFOLDF(reassoc_minmax_left)
> -{
> -  if (fins->op2 == fleft->op1 || fins->op2 == fleft->op2)
> -    return RIGHTFOLD;  /* (b o1 a) o2 b ==> b; (a o1 b) o2 b ==> b */
> -  return NEXTFOLD;
> -}

This particular patch (reassoc_minmax_left) fixes the following test
case on aarch64. (TTTT)

| LUA_PATH="src/?.lua;;" src/luajit -Ohotloop=1 -e '
| local min = math.min
| local max = math.max
| local nan = 0/0
|
| local r_assoc = {}
| local r_assoc2 = {}
| print("MIN - MAX:")
| for i = 1, 4 do
|   r_assoc[i]  =  min(max(nan, 1), 1)
|   r_assoc2[i] =  min(max(1, nan), 1)
| end
| print(r_assoc[1],  r_assoc[2],  r_assoc[3],  r_assoc[4])
| print(r_assoc2[1], r_assoc2[2], r_assoc2[3], r_assoc2[4], "\n")
|
| print("MAX - MIN:")
| for i = 1, 4 do
|   r_assoc[i]  =  max(min(nan, 1), 1)
|   r_assoc2[i] =  max(min(1, nan), 1)
| end
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4])
| print(r_assoc2[1], r_assoc2[2], r_assoc2[3], r_assoc2[4], "\n")
| '

Returns before the patch:
| MIN - MAX:
| 1       1       1       nan
| 1       1       1       nan
|
| MAX - MIN:
| 1       1       nan     nan
| 1       1       nan     nan

And after the patch:

| MIN - MAX:
| 1       1       1       1
| 1       1       1       1
|
| MAX - MIN:
| 1       1       1       1
| 1       1       1       1

> -
> -LJFOLD(MIN any MAX)
> -LJFOLD(MAX any MIN)
> -LJFOLDF(reassoc_minmax_right)
> -{
> -  if (fins->op1 == fright->op1 || fins->op1 == fright->op2)
> -    return LEFTFOLD;  /* a o2 (a o1 b) ==> a; a o2 (b o1 a) ==> a */
> -  return NEXTFOLD;
> -}
> -

OK, let's try a similar test case here (TTTTT):

| LUA_PATH="src/?.lua;;" src/luajit -Ohotloop=1 -e '
| local min = math.min
| local max = math.max
| local nan = 0/0
|
| local r_assoc = {}
| local r_assoc2 = {}
| print("MIN - MAX:")
| for i = 1, 4 do
|   r_assoc[i]  =  min(1, max(nan, 1))
|   r_assoc2[i] =  min(1, max(1, nan))
| end
| print(r_assoc[1],  r_assoc[2],  r_assoc[3],  r_assoc[4])
| print(r_assoc2[1], r_assoc2[2], r_assoc2[3], r_assoc2[4], "\n")
|
| print("MAX - MIN:")
| for i = 1, 4 do
|   r_assoc[i]  =  max(1, min(nan, 1))
|   r_assoc2[i] =  max(1, min(1, nan))
| end
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4])
| print(r_assoc2[1], r_assoc2[2], r_assoc2[3], r_assoc2[4], "\n")
| '

Before the patch it leads to the assertion failure in
`rec_check_slots()` (both arches):
| MIN - MAX:
| luajit: src/lj_record.c:142: rec_check_slots: Assertion `lj_obj_equal(tv, &tvk)' failed.
| Aborted

After the patch we got inconsistent results on x86/x64 (same results in
the upstream):

| MIN - MAX:
| 1       1       1       1
| nan     nan     nan     1
|
| MAX - MIN:
| 1       1       1       1
| nan     nan     1       1

But for aarch64 it is fine:

| MIN - MAX:
| 1       1       1       1
| nan     nan     nan     nan
|
| MAX - MIN:
| 1       1       1       1
| nan     nan     nan     nan

I suggest to add this testcase without checking values consistency, but
with FIXME: mark.

>  /* -- Array bounds check elimination -------------------------------------- */
>  
>  /* Eliminate ABC across PHIs to handle t[i-1] forwarding case.
> @@ -2018,8 +1996,6 @@ LJFOLDF(comm_comp)
>  
>  LJFOLD(BAND any any)
>  LJFOLD(BOR any any)
> -LJFOLD(MIN any any)
> -LJFOLD(MAX any any)
>  LJFOLDF(comm_dup)
>  {
>    if (fins->op1 == fins->op2)  /* x o x ==> x */
> @@ -2027,6 +2003,15 @@ LJFOLDF(comm_dup)
>    return fold_comm_swap(J);
>  }
>  
> +LJFOLD(MIN any any)
> +LJFOLD(MAX any any)
> +LJFOLDF(comm_dup_minmax)
> +{
> +  if (fins->op1 == fins->op2)  /* x o x ==> x */
> +    return LEFTFOLD;
> +  return NEXTFOLD;
> +}
> +

No need to swap operands here to avoid side effects, so just continue.
This fixes (TT) on arm64.

We get the following mcode now:
| fcmp  d2, d3         ; fcmp 1.0, nan
| fcsel d1, d3, d2, pl ; d1 == nan after this instruction
| ...
| fcmp  d2, d1         ; fcmp 1.0, nan
| fcsel d0, d1, d2, pl ; d0 == nan after this instruction

`pl` (aka `CC_PL`) condition means that N flag is 0 [2], that
is true when we are comparing something with NaN. So, the value of the
first source register is taken.

After this particular chunk of the patch (comm_dup_minmax) plus
(reassoc_dup_minmax) (TT) output is the following on aarch64:

| 1       nan     inf
| MIN:    1       nan
| nan     nan     nan     nan
| nan     nan     nan     nan

>  LJFOLD(BXOR any any)
>  LJFOLDF(comm_bxor)
>  {
> diff --git a/src/lj_vmmath.c b/src/lj_vmmath.c
> index c04459bd..ae4e0f15 100644
> --- a/src/lj_vmmath.c
> +++ b/src/lj_vmmath.c
> @@ -49,8 +49,8 @@ double lj_vm_foldarith(double x, double y, int op)
>    case IR_ABS - IR_ADD: return fabs(x); break;
>  #if LJ_HASJIT
>    case IR_LDEXP - IR_ADD: return ldexp(x, (int)y); break;
> -  case IR_MIN - IR_ADD: return x > y ? y : x; break;
> -  case IR_MAX - IR_ADD: return x < y ? y : x; break;
> +  case IR_MIN - IR_ADD: return x < y ? x : y; break;
> +  case IR_MAX - IR_ADD: return x > y ? x : y; break;

(TTTTTT) This piece of the patch fixes inconsistency in
`fold_kfold_numarith()` (on x86/x64):

| $ LUA_PATH="src/?.lua;;" src/luajit -jdump=i -Ohotloop=1 -e '
| local min = math.min
| local max = math.max
|
| local r_assoc = {}
| print("MIN:")
| for i = 1, 4 do
|   r_assoc[i] =  min(min(7.1, 0/0), 1.1)
| end
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4], "\n")
|
| print("MAX:")
| for i = 1, 4 do
|   r_assoc[i] =  max(max(7.1, 0/0), 1.1)
| end
| print(r_assoc[1], r_assoc[2], r_assoc[3], r_assoc[4])
| '
| MIN:
| ---- TRACE 1 start (command line):7
| luajit: /home/burii/reviews/luajit/minmax/src/lj_record.c:142: rec_check_slots: Assertion `lj_obj_equal(tv, &tvk)' failed.
| Aborted
NB: use 0/0 constant here according to `fold_kfold_numarith()`
semantics.

`tv` is nan(0x8000000000000), `tvk` is 7.1.

If we look in disassembled code of `lj_vm_foldarith()` we can see the
following:

| /* In our example x == 7.1, y == nan */
| case IR_MIN - IR_ADD: return x > y ? y : x; break;
| case IR_MAX - IR_ADD: return x < y ? y : x; break;

| ; case IR_MIN
| <lj_vm_foldarith+337>:  movsd  xmm0,QWORD PTR [rsp+0x18] ; xmm0 <- 7.1
| <lj_vm_foldarith+343>:  comisd xmm0,QWORD PTR [rsp+0x10] ; comisd 7.1, nan
| <lj_vm_foldarith+349>:  jbe    <lj_vm_foldarith+358>     ; >= ?
| <lj_vm_foldarith+351>:  mov    rax,QWORD PTR [rsp+0x10]  ; return nan
| <lj_vm_foldarith+356>:  jmp    <lj_vm_foldarith+398>     ;
| <lj_vm_foldarith+358>:  mov    rax,QWORD PTR [rsp+0x18]  ; else return 7.1
| <lj_vm_foldarith+363>:  jmp    <lj_vm_foldarith+398>     ;
| ; case IR_MAX
| <lj_vm_foldarith+365>:  movsd  xmm0,QWORD PTR [rsp+0x10] ; xmm0 <- nan
| <lj_vm_foldarith+371>:  comisd xmm0,QWORD PTR [rsp+0x18] ; comisd nan, 7.1
| <lj_vm_foldarith+377>:  jbe    <lj_vm_foldarith+386>     ; >= ?
| <lj_vm_foldarith+379>:  mov    rax,QWORD PTR [rsp+0x10]  ; return nan
| <lj_vm_foldarith+384>:  jmp    <lj_vm_foldarith+398>     ;
| <lj_vm_foldarith+386>:  mov    rax,QWORD PTR [rsp+0x18]  ; else return 7.1
| <lj_vm_foldarith+391>:  jmp    <lj_vm_foldarith+398>     ;

According to `comisd` documentation [4] in case when the one operand is NaN,
the result is unordered and ZF,PF,CF := 111. This means that `jbe`
condition is true (CF=1 or ZF=1)[5], so we return 7.1 (the first
operand) for case `IR_MIN`.

Q: Why it is the problem?
For answer lets see the `lj_ff_math_min()` and `lj_ff_math_max()` in the VM:

For number values we got the following:
|7:
|  sseop xmm0, xmm1
Where `sseop` is `minsd`/`maxsd` instruction correspondingly.
From the instruction reference guides [6][7]:
| If only one value is a NaN (SNaN or QNaN) for this instruction, the
| second source operand, either a NaN or a valid floating-point value, is
| written to the result.

After the patch we got the following disassembled code for
`lj_vm_foldarith()`:

| ; case IR_MIN
| <lj_vm_foldarith+337>:  movsd  xmm0,QWORD PTR [rsp+0x10] ; xmm0 <- nan
| <lj_vm_foldarith+343>:  comisd xmm0,QWORD PTR [rsp+0x18] ; comisd nan, 7.1
| <lj_vm_foldarith+349>:  jbe    <lj_vm_foldarith+358>     ; >= ?
| <lj_vm_foldarith+351>:  mov    rax,QWORD PTR [rsp+0x18]  ; return 7.1
| <lj_vm_foldarith+356>:  jmp    <lj_vm_foldarith+398>     ;
| <lj_vm_foldarith+358>:  mov    rax,QWORD PTR [rsp+0x10]  ; else return nan
| <lj_vm_foldarith+363>:  jmp    <lj_vm_foldarith+398>     ;
| ; case IR_MAX
| <lj_vm_foldarith+365>:  movsd  xmm0,QWORD PTR [rsp+0x18] ; xmm0 <- 7.1
| <lj_vm_foldarith+371>:  comisd xmm0,QWORD PTR [rsp+0x10] ; comisd 7.1, nan
| <lj_vm_foldarith+377>:  jbe    <lj_vm_foldarith+386>     ; >= ?
| <lj_vm_foldarith+379>:  mov    rax,QWORD PTR [rsp+0x18]  ; return 7.1
| <lj_vm_foldarith+384>:  jmp    <lj_vm_foldarith+398>     ;
| <lj_vm_foldarith+386>:  mov    rax,QWORD PTR [rsp+0x10]  ; else return nan
| <lj_vm_foldarith+391>:  jmp    <lj_vm_foldarith+398>     ;

So now we always return the second operand (nan for case `IR_MIN`).

Side note: IMHO, instead of this behavior, it the NaN source operand
should be returned. Also, IINM, it makes the behaviour consistent with
PUC-Rio Lua 5.1.

NB:

For aarch64 this changes the follwing assembly:
| 62630: ldr     d1, [sp, #40]
| 62634: ldr     d0, [sp, #32]
| 62638: fcmpe   d1, d0
| 6263c: b.le    62648 <lj_vm_foldarith+0x150>
| 62640: ldr     d0, [sp, #32]
| 62644: b       62674 <lj_vm_foldarith+0x17c>
| 62648: ldr     d0, [sp, #40]
| 6264c: b       62674 <lj_vm_foldarith+0x17c>
| 62650: ldr     d1, [sp, #40]
| 62654: ldr     d0, [sp, #32]
| 62658: fcmpe   d1, d0
| 6265c: b.pl    62668 <lj_vm_foldarith+0x170>  // b.nfrst
| 62660: ldr     d0, [sp, #32]
| 62664: b       62674 <lj_vm_foldarith+0x17c>
| 62668: ldr     d0, [sp, #40]
| 6266c: b       62674 <lj_vm_foldarith+0x17c>

To this one:

| 62620: ldr     d1, [sp, #40]
| 62624: ldr     d0, [sp, #32]
| 62628: fcmpe   d1, d0
| 6262c: b.pl    62638 <lj_vm_foldarith+0x150>  // b.nfrst
| 62630: ldr     d0, [sp, #40]
| 62634: b       62664 <lj_vm_foldarith+0x17c>
| 62638: ldr     d0, [sp, #32]
| 6263c: b       62664 <lj_vm_foldarith+0x17c>
| 62640: ldr     d1, [sp, #40]
| 62644: ldr     d0, [sp, #32]
| 62648: fcmpe   d1, d0
| 6264c: b.le    62658 <lj_vm_foldarith+0x170>
| 62650: ldr     d0, [sp, #40]
| 62654: b       62664 <lj_vm_foldarith+0x17c>
| 62658: ldr     d0, [sp, #32]
| 6265c: b       62664 <lj_vm_foldarith+0x17c>

So, we should provide the same changes for VM|JIT generated mcode (and
this is why CC_LO/CC_HI are replaced with CC_LE/CC_PL)

>  #endif
>    default: return x;
>    }
> diff --git a/src/vm_arm.dasc b/src/vm_arm.dasc
> index a29292f1..89faa03e 100644
> --- a/src/vm_arm.dasc
> +++ b/src/vm_arm.dasc
> @@ -1718,8 +1718,8 @@ static void build_subroutines(BuildCtx *ctx)
>    |.endif
>    |.endmacro
>    |
> -  |  math_minmax math_min, gt, hi
> -  |  math_minmax math_max, lt, lo
> +  |  math_minmax math_min, gt, pl
> +  |  math_minmax math_max, lt, le
>    |
>    |//-- String library -----------------------------------------------------
>    |
> diff --git a/src/vm_arm64.dasc b/src/vm_arm64.dasc
> index f517a808..2c1bb4f8 100644
> --- a/src/vm_arm64.dasc
> +++ b/src/vm_arm64.dasc
> @@ -1494,8 +1494,8 @@ static void build_subroutines(BuildCtx *ctx)
>    |  b <6
>    |.endmacro
>    |
> -  |  math_minmax math_min, gt, hi
> -  |  math_minmax math_max, lt, lo
> +  |  math_minmax math_min, gt, pl
> +  |  math_minmax math_max, lt, le
>    |
>    |//-- String library -----------------------------------------------------
>    |
> diff --git a/src/vm_x64.dasc b/src/vm_x64.dasc
> index 59f117ba..faeb5181 100644
> --- a/src/vm_x64.dasc
> +++ b/src/vm_x64.dasc

<snipped>

> diff --git a/src/vm_x86.dasc b/src/vm_x86.dasc
> index f7ffe5d2..1c995d16 100644
> --- a/src/vm_x86.dasc
> +++ b/src/vm_x86.dasc

<snipped>

> diff --git a/test/tarantool-tests/gh-6163-min-max.test.lua b/test/tarantool-tests/gh-6163-min-max.test.lua
> new file mode 100644
> index 00000000..1da8a259
> --- /dev/null
> +++ b/test/tarantool-tests/gh-6163-min-max.test.lua
> @@ -0,0 +1,48 @@
> +local tap = require('tap')
> +local test = tap.test('gh-6163-jit-min-max')
> +test:plan(3)
> +--
> +-- gh-6163: math.min/math.max inconsistencies.
> +--
> +
> +local function is_consistent(res)
> +  for i = 1, #res - 1 do
> +    if res[i] ~= res[i + 1] then
> +      return false
> +    end
> +  end
> +  return true
> +end

Side note: may be it is better to name it `array_is_consistent()` as far
as it checks only array table part?
Also, we probably can use it in our test utils.

> +
> +-- This function creates dirty values on the Lua stack.
> +-- The latter of them is going to be treated as an
> +-- argument by the `math.min/math.max`.
> +-- The first two of them are going to be overwritten
> +-- by the math function itself.
> +local function filler()
> +  return 1, 1, 1
> +end
> +
> +-- Success with no args.

Don't get the comment.
Do you mean:
| -- `math.min()` should raise an error when is called without
| -- arguments.

> +filler()
> +local r, _ = pcall(function() math.min() end)
> +test:ok(false == r, 'math.min fails with no args')

Why don't use `not r` here?

> +
> +filler()
> +r, _ = pcall(function() math.max() end)
> +test:ok(false == r, 'math.max fails with no args')

Why do we need the second test?
Please, add the coressponding comment.

> +
> +-- Incorrect fold optimization.

Don't get your comment. What do you mean?

> +jit.off()
> +jit.flush()

Why do we need to remove previous traces before start?

> +jit.opt.start('hotloop=1')
> +jit.on()
> +
> +local res = {}
> +for i = 1, 4 do
> +  res[i] = math.min(math.min(0/0, math.huge), math.huge)

NB: IINM, still inconsistent if we use
| local nan = 0/0
| -- ...
|   res[i] = math.min(math.min(nan, math.huge), math.huge)

I suppose, that we should comment this.

> +end
> +
> +test:ok(is_consistent(res), '(a o b) o a -> a o b')
> +
> +os.exit(test:check() and 0 or 1)
> -- 
> 2.38.1
> 

[1]: https://developer.arm.com/documentation/dui0801/g/A64-Floating-point-Instructions/FCMP
[2]: https://www.cs.princeton.edu/courses/archive/spr19/cos217/reading/ArmInstructionSetOverview.pdf
[3]: https://developer.arm.com/documentation/100069/0608/A64-Floating-point-Instructions/FCSEL
[4]: https://www.felixcloutier.com/x86/comisd
[5]: https://www.felixcloutier.com/x86/jcc
[6]: https://www.felixcloutier.com/x86/minsd
[7]: https://www.felixcloutier.com/x86/maxsd
[8]: https://github.com/tarantool/luajit/commit/9b6c0cd8eafdd2e5a8a7ac4b33f6e33b3d8a93b9

-- 
Best regards,
Sergey Kaplun


      reply	other threads:[~2022-12-23 15:46 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-12-19  9:52 Maxim Kokryashkin via Tarantool-patches
2022-12-23 15:42 ` Sergey Kaplun via Tarantool-patches [this message]

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=Y6XMebt03ffqqDPQ@root \
    --to=tarantool-patches@dev.tarantool.org \
    --cc=max.kokryashkin@gmail.com \
    --cc=skaplun@tarantool.org \
    --subject='Re: [Tarantool-patches] [PATCH luajit v6] Fix math.min()/math.max() inconsistencies.' \
    /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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox