Skip to content

Commit

Permalink
Merge pull request #273 from daemanos/new-overflow
Browse files Browse the repository at this point in the history
Add new overflow-checking primitives

Motivation

MLton currently implements checked arithmetic with specialized primitives.
These primitives are exposed to the Basis Library implementation as functions
that implicitly raise a primitive exception:

    val +! = _prim "WordS8_addCheck": int * int -> int;

In the XML IR, special care is taken to "remember" the primive exception
associated with these primitives in order to implement exceptions correctly.  In
the SSA, SSA2, RSSA, and Machine IRs, these primitives are treated as transfers,
rather than statements.

This pull request implements a possibly better implementation of these
operations as simple primitives that return a boolean:

    val +! = _prim "Word8_add": int * int -> int;
    val +? = _prim "WordS8_addCheckP": int * int -> bool;
    val +$ = fn (x, y) => let val r = +! (x, y)
                          in  if +? (x, y) then raise Overflow else r
                          end

This would eliminate the special cases in the XML IR and in the SSA, SSA2, RSSA,
and Machine IRs, where the primitives would be treated as statements.  Other
compilers provide overflow checking via boolean-returning functions:

 * https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html
 * https://llvm.org/docs/LangRef.html#arithmetic-with-overflow-intrinsics

Implementation

This patch refactors the primitive checked-arithmetic operations such that the
suffix `?` represents a new overflow-checking predicate, a suffix `!` represents
the non-overflow-checking variant, and a suffix `$` represents the
overflow-checking variant (mnemonic: `$` for "safe" or "expensive"). The
behavior of the new `$`-operations is controlled by a compile-time constant,
`MLton.newOverflow`.  When set to false (the default), the `$`-operations make
use of the old-style implicit-`Overflow` primitives.  When set to true, the
`$`-operations are implemented as an `if`-expression that branches on the result
of the corresponding `?`-operation and either raises the `Overflow` exception or
returns the result of the corresponding `!`-operation.  Finally, the bare
operation is aliased to either the `$`-form (with overflow detection enabled) or
the `!`-form (with overflow detection disabled).  Essentially:

    val +! = _prim "Word8_add": int * int -> int;
    val +? = _prim "WordS8_addCheckP": int * int -> bool;
    val +$ = if MLton.newOverflow
             then fn (x, y) => let val r = +! (x, y)
                               in  if +? (x, y) then raise Overflow else r
                               end
             else fn (x, y) => ((_prim "WordS8_addCheckP": int * int -> int;) (x, y))
                               handle PrimOverflow => raise Overflow
    val + = if MLton.detectOverflow then +$ else +!

Note that the checked-arithmetic using `!`- and `$`-operations is careful to
perform the `!`-operation before the `$`-operation.  With the native-codegens, a
new peephole optimization combines the separate unchecked-arithmetic operation
and checked-arithmetic-predicate operation into a single instruction.  For the
C-codgen, the new checked-arithmetic-predicate primitives are translated to uses
of the `__builtin_{add,sub,mul}_overflow` intrinsics (which improves upon the
previous explicit conditional checking, but requires gcc 5 or greater).
Similarly, for the LLVM-codgen, the new checked-arithmetic-predicate primitives
are translated to uses of the `{sadd,uadd,smul,umul,ssub,usub}.with.overflow`
intrinsics.  For both the C- and LLVM-codegens, it is expected that these
intrinsics will be combined with the separate unchecked-arithmetic operation.

In addition, the `RedundantTests` optimization has been extended to eliminate
the overflow test when adding or subtracting 1 with the new primitives.

Performance

The native-codegen peephole optimization and `RedundantTests` have been mostly
sufficient to keep performance on par with the older checked-arithmetic
primitives, and in some cases performance has even significantly improved.  Below
is a summary of the exceptional runtime ratios in the different codegens (both
positive and negative):

| Benchmark       | Native | LLVM |    C |
|-----------------|--------|------|------|
| even-odd        |   1.00 | 1.00 | 1.09 |
| hamlet          |   0.98 | 0.90 | 0.93 |
| imp-for         |   0.99 | 1.50 | 0.46 |
| lexgen          |   0.92 | 1.31 | 1.24 |
| matrix-multiply |   0.99 | 1.00 | 0.87 |
| md5             |   1.06 | 1.01 | 0.97 |
| tensor          |   1.01 | 1.00 | 0.57 |

No benchmarks were consistently worse or better on all codegen, e.g., the
`imp-for` benchmark performed exceptionally badly on the LLVM codegen, but was
much faster on the C codegen and about even on the native codegen.  For this
particular benchmark, the cause of the slowdown with the LLVM codegen has yet to
be discovered.  Similarly, the cause of the slowdown in `lexgen` with the C- and
LLVM-codegens is unknown.  For the `md5` benchmark, on the other hand, the cause
of the slowdown with the native codegen seems to be a failure to eliminate
common subexpressions in certain circumstances, which can potentially be
improved in the future.  Improvements in the C-codegen are likely to be due to
the better overflow checking with builtins.

Future work

The `CommonSubexp` optimization currently handles `Arith` transfers specially;
in particular, the result of an `Arith` transfer can be used in common `Arith`
transfers that it dominates.  This was done so that code like:

    (n + m) + (n + m)

can be transformed to

    let val r = n + m in r + r end

With the new checked-arithmetic-predicate primitives, the computation of the
boolean value may be common-subexpr eliminated, but `Case` discrimination will
not.  This forces the boolean value to be reified and to be discriminated
multiple times (though, perhaps `KnownCase` could eliminate subsequent
discriminations).  Extending `CommonSubexpr` to propagate flow-sensitive
relationships at `Case` transfers to the dominated targets could improve the
performance `md5` with `MLton.newOverflow true` and potentially improve
performance elsewhere as well (e.g., by propagating the results of comparisons
as well).

Once all performance slowdowns with `MLton.newOverflow true` have been
eliminated, it would be desirable to remove the old-style implicit-`Overflow`
primitives and `Arith` transfers.  This would eliminate many previous instances
of special-case code to handle these primitives and transfers.

Finally, it may be worth investigating an implementation of the
checked-operations via

    val +$ = fn (x, y) => let val b = +? (x, y)
                              val r = +! (x, y)
                          in  if b then raise Overflow else r
                          end

rather than

    val +$ = fn (x, y) => let val r = +! (x, y)
                          in  if +? (x, y) then raise Overflow else r
                          end

The advantage of calculating the boolean first is that when `x` (or `y`) is a
loop variable and `r` will be passed as the value for the next iteration, then
both `x` and `r` could be assigned the same location (pseudo-register or stack
slot).  `x` and `r` cannot share the same location when the boolean is
calculated second, because `x` and `r` are both live at the calculation of the
boolean.  See #218.  This would require reworking the native-codegen
peephole optimization.
  • Loading branch information
MatthewFluet committed Oct 15, 2018
2 parents e149c99 + 2dc3968 commit 0b04074
Show file tree
Hide file tree
Showing 49 changed files with 5,399 additions and 546 deletions.
44 changes: 36 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,49 @@ sudo: required
matrix:
include:
- os: linux
env: CC=gcc MLTON_COMPILE_ARGS="-codegen amd64" REGRESSION=true
dist: trusty
addons:
apt:
update: true
sources:
- ubuntu-toolchain-r-test
packages:
- gcc-5
- mlton
env: CC=gcc-5 MLTON_COMPILE_ARGS="-codegen amd64" REGRESSION=true
- os: linux
env: CC=gcc MLTON_COMPILE_ARGS="-codegen c" REGRESSION=false
dist: trusty
addons:
apt:
update: true
sources:
- ubuntu-toolchain-r-test
packages:
- gcc-5
- mlton
env: CC=gcc-5 MLTON_COMPILE_ARGS="-codegen c" REGRESSION=false
- os: linux
env: CC=clang MLTON_COMPILE_ARGS="-codegen c" REGRESSION=false
dist: trusty
addons:
apt:
update: true
packages:
- mlton
env: CC=clang MLTON_COMPILE_ARGS="-codegen c" REGRESSION=false
- os: linux
env: CC=clang MLTON_COMPILE_ARGS="-codegen llvm" REGRESSION=false
dist: trusty
addons:
apt:
update: true
packages:
- mlton
env: CC=clang MLTON_COMPILE_ARGS="-codegen llvm" REGRESSION=false
- os: osx
env: CC=clang MLTON_COMPILE_ARGS="-codegen amd64" REGRESSION=false
env: CC=clang MLTON_COMPILE_ARGS="-codegen amd64" REGRESSION=false
- os: osx
env: CC=clang MLTON_COMPILE_ARGS="-codegen c" REGRESSION=false
env: CC=clang MLTON_COMPILE_ARGS="-codegen c" REGRESSION=false

install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install -qq mlton; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install mlton; fi

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Here are the changes from versoin 20180206 to version YYYYMMDD.

=== Details

* 2018-10-15
** Introduce new `Overflow`-checking primitives. Undertaken by Daman Morris
at RIT supported by NSF CISE Research Infrastructure (CRI) award.

* 2018-08-17
** Add a parser for the SSA2 IR (`functor ParseSsa2`). Undertaken by Manan
Joshi at RIT supported by NSF CISE Research Infrastructure (CRI) award.
Expand Down
38 changes: 19 additions & 19 deletions basis-library/arrays-and-vectors/array2.sml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
structure Array2 : ARRAY2 =
struct

val op +? = SeqIndex.+?
val op +! = SeqIndex.+!
val op + = SeqIndex.+
val op -? = SeqIndex.-?
val op -! = SeqIndex.-!
val op - = SeqIndex.-
val op *? = SeqIndex.*?
val op *! = SeqIndex.*!
val op * = SeqIndex.*
val op < = SeqIndex.<
val op <= = SeqIndex.<=
Expand Down Expand Up @@ -66,12 +66,12 @@ structure Array2 : ARRAY2 =
handle Overflow => raise Subscript
in
if (start < 0 orelse num < 0
orelse start +? num > max)
orelse start +! num > max)
then raise Subscript
else (start, start +? num)
else (start, start +! num)
end
else (SeqIndex.fromIntUnsafe start,
SeqIndex.fromIntUnsafe start +? num)
SeqIndex.fromIntUnsafe start +! num)
fun checkSliceMax (start: int,
num: int option,
max: SeqIndex.int): SeqIndex.int * SeqIndex.int =
Expand Down Expand Up @@ -147,7 +147,7 @@ structure Array2 : ARRAY2 =
cols = 0}

fun unsafeSpot' ({cols, ...}: 'a array, r, c) =
r *? cols +? c
r *! cols +! c
fun spot' (a as {rows, cols, ...}: 'a array, r, c) =
if Primitive.Controls.safe
andalso (geu (r, rows) orelse geu (c, cols))
Expand Down Expand Up @@ -197,13 +197,13 @@ structure Array2 : ARRAY2 =
List.foldl
(fn (row: 'a list, i) =>
let
val max = i +? cols'
val max = i +! cols'
val i' =
List.foldl (fn (x: 'a, i) =>
(if i >= max
then raise Size
else (Primitive.Array.unsafeUpdate (array, i, x)
; i +? 1)))
; i +! 1)))
i row
in if i' = max
then i'
Expand All @@ -218,7 +218,7 @@ structure Array2 : ARRAY2 =
if Primitive.Controls.safe andalso geu (r, rows)
then raise Subscript
else
ArraySlice.vector (Primitive.Array.Slice.slice (array, r *? cols, SOME cols))
ArraySlice.vector (Primitive.Array.Slice.slice (array, r *! cols, SOME cols))
fun row (a, r) =
if Primitive.Controls.safe
then let
Expand Down Expand Up @@ -257,9 +257,9 @@ structure Array2 : ARRAY2 =
else let
fun loopCol (c, b) =
if c >= stopCol then b
else loopCol (c +? 1, f (r, c, sub' (base, r, c), b))
else loopCol (c +! 1, f (r, c, sub' (base, r, c), b))
in
loopRow (r +? 1, loopCol (startCol, b))
loopRow (r +! 1, loopCol (startCol, b))
end
in
loopRow (startRow, b)
Expand All @@ -271,9 +271,9 @@ structure Array2 : ARRAY2 =
else let
fun loopRow (r, b) =
if r >= stopRow then b
else loopRow (r +? 1, f (r, c, sub' (base, r, c), b))
else loopRow (r +! 1, f (r, c, sub' (base, r, c), b))
in
loopCol (c +? 1, loopRow (startRow, b))
loopCol (c +! 1, loopRow (startRow, b))
end
in
loopCol (startCol, b)
Expand Down Expand Up @@ -310,8 +310,8 @@ structure Array2 : ARRAY2 =
dst, dst_row, dst_col} =
let
val {startRow, stopRow, startCol, stopCol} = checkRegion src
val nrows = stopRow -? startRow
val ncols = stopCol -? startCol
val nrows = stopRow -! startRow
val ncols = stopCol -! startCol
val {startRow = dst_row, startCol = dst_col, ...} =
checkRegion' {base = dst, row = dst_row, col = dst_col,
nrows = SOME nrows,
Expand All @@ -330,13 +330,13 @@ structure Array2 : ARRAY2 =
if i < start
then ()
else (f i; loop (i - 1))
in loop (stop -? 1)
in loop (stop -! 1)
end
val forRows = if startRow <= dst_row then forDown else forUp
val forCols = if startCol <= dst_col then forUp else forDown
in forRows (0, nrows, fn r =>
forCols (0, ncols, fn c =>
unsafeUpdate' (dst, dst_row +? r, dst_col +? c,
unsafeSub' (base, startRow +? r, startCol +? c))))
unsafeUpdate' (dst, dst_row +! r, dst_col +! c,
unsafeSub' (base, startRow +! r, startCol +! c))))
end
end
48 changes: 24 additions & 24 deletions basis-library/arrays-and-vectors/sequence.fun
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ structure SeqIndex =

functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
struct
val op +? = SeqIndex.+?
val op +! = SeqIndex.+!
val op -? = SeqIndex.-?
val op +$ = SeqIndex.+$
val op -! = SeqIndex.-!
val op <= = SeqIndex.<=
val op > = SeqIndex.>
val op >= = SeqIndex.>=
Expand Down Expand Up @@ -246,9 +246,9 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
val add =
if Primitive.Controls.safe
then (fn (x, s) =>
(s +! S.Slice.length (toSlice x))
(s +$ S.Slice.length (toSlice x))
handle Overflow => raise Size)
else (fn (x, s) => s +? S.Slice.length (toSlice x))
else (fn (x, s) => s +! S.Slice.length (toSlice x))
val n = List.foldl add 0 xs
val a = Primitive.Array.alloc n
fun loop (di, xs) =
Expand All @@ -258,7 +258,7 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
let val sl = toSlice x
in
S.Slice.unsafeCopy {dst = a, di = di, src = sl}
; loop (di +? S.Slice.length sl, xs)
; loop (di +! S.Slice.length sl, xs)
end
in
loop (0, xs)
Expand All @@ -276,10 +276,10 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
val add =
if Primitive.Controls.safe
then (fn (x, s) =>
(s +! sepn +! S.Slice.length (toSlice x))
(s +$ sepn +$ S.Slice.length (toSlice x))
handle Overflow => raise Size)
else (fn (x, s) =>
(s +? sepn +? S.Slice.length (toSlice x)))
(s +! sepn +! S.Slice.length (toSlice x)))
val n = List.foldl add (S.Slice.length (toSlice x)) xs
val a = Primitive.Array.alloc n
fun loop (di, xs) =
Expand All @@ -296,9 +296,9 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
let
val sl = toSlice x
val _ = S.Slice.unsafeCopy {dst = a, di = di, src = sl}
val di = di +? S.Slice.length sl
val di = di +! S.Slice.length sl
val _ = S.Slice.unsafeCopy {dst = a, di = di, src = sep}
val di = di +? sepn
val di = di +! sepn
in
loop (di, xs)
end
Expand All @@ -318,7 +318,7 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
in
if SeqIndex.> (k, len)
then S.Slice.unsafeSubslice (sl, len, SOME 0)
else S.Slice.unsafeSubslice (sl, k, SOME (len -? k))
else S.Slice.unsafeSubslice (sl, k, SOME (len -! k))
end handle Overflow =>
(* k is positive, so behavior is specified! *)
S.Slice.unsafeSubslice (sl, S.Slice.length sl, SOME 0)
Expand All @@ -334,7 +334,7 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
in
if SeqIndex.> (k, len)
then S.Slice.unsafeSubslice (sl, 0, SOME 0)
else S.Slice.unsafeSubslice (sl, 0, SOME (len -? k))
else S.Slice.unsafeSubslice (sl, 0, SOME (len -! k))
end handle Overflow =>
(* k is positive, so behavior is specified! *)
S.Slice.unsafeSubslice (sl, 0, SOME 0)
Expand All @@ -347,16 +347,16 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
in
if n <= n'
then let
val n'' = n' -? n
val n'' = n' -! n
fun loop (i, j) =
if i > n''
then false
else if j >= n
then true
else if eq (S.unsafeSub (seq, j),
S.Slice.unsafeSub (sl, i +? j))
then loop (i, j +? 1)
else loop (i +? 1, 0)
S.Slice.unsafeSub (sl, i +! j))
then loop (i, j +! 1)
else loop (i +! 1, 0)
in
loop (0, 0)
end
Expand All @@ -376,7 +376,7 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
then true
else if eq (S.unsafeSub (seq, j),
S.Slice.unsafeSub (sl, j))
then loop (j +? 1)
then loop (j +! 1)
else false
in
loop (0)
Expand All @@ -392,13 +392,13 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
in
if n <= n'
then let
val n'' = n' -? n
val n'' = n' -! n
fun loop (j) =
if j >= n
then true
else if eq (S.unsafeSub (seq, j),
S.Slice.unsafeSub (sl, n'' +? j))
then loop (j +? 1)
S.Slice.unsafeSub (sl, n'' +! j))
then loop (j +! 1)
else false
in
loop (0)
Expand All @@ -423,14 +423,14 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
fun make finish p sl =
let
val (seq, start, len) = S.Slice.base sl
val max = start +? len
val max = start +! len
fun loop (i, start, sls) =
if i >= max
then List.rev (finish (seq, start, i, sls))
else
if p (S.unsafeSub (seq, i))
then loop (i +? 1, i +? 1, finish (seq, start, i, sls))
else loop (i +? 1, start, sls)
then loop (i +! 1, i +! 1, finish (seq, start, i, sls))
else loop (i +! 1, start, sls)
in loop (start, start, [])
end
in
Expand All @@ -441,14 +441,14 @@ functor Sequence (S: PRIM_SEQUENCE): SEQUENCE =
else
(fromSlice
(S.Slice.unsafeSlice
(seq, start, SOME (stop -? start))))
(seq, start, SOME (stop -! start))))
:: sls)
p sl
fun fieldsGen fromSlice p sl =
make (fn (seq, start, stop, sls) =>
(fromSlice
(S.Slice.unsafeSlice
(seq, start, SOME (stop -? start))))
(seq, start, SOME (stop -! start))))
:: sls)
p sl
end
Expand Down
Loading

0 comments on commit 0b04074

Please sign in to comment.