Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unsafe operations for array uninitialization and raw arrays #207

Merged
merged 27 commits into from Oct 21, 2017

Conversation

MatthewFluet
Copy link
Member

@MatthewFluet MatthewFluet commented Oct 21, 2017

Highlights:

  • Rename Array_uninit primitive to Array_alloc (612eede)
  • Add Array_uninit: 'a array * SeqInt.int -> unit primitive (946385f)
  • Add Array_uninitIsNop: 'a array -> bool primitive (426baa1)
  • Add Array_allocRaw: SeqIndex.int -> 'a array and Array_toArray: 'a array -> 'a array primitives (580da00)

All new primitives are implemented in toRssa.

The Array_uninit primitive sets all objptrs in the element at the given index to a bogus non-objptr value (0wx1). One motivation for this primitive is to support space-efficient polymorphic resizeable arrays. When shrinking a resizeable array, we would like to "NULL" out the elements that are no longer part of the logical array, in order to avoid a (logical) space leak.

The Array_uninitIsNop primitive answers if the Array_uninit primitive applied to the same array would be a nop (i.e., if the array has no objptrs in the elements). This can be used to skip a bulk-Array_uninit loop when it is known that the Array_uninit operations would be nops.

It would be possible to introduce (pre-toRssa) optimizations that simplified Array_uninitIsNop and Array_uninit when it is clear that the array will never have objptrs in the elements.

The Array_allocRaw primitive allocates an array, but with a header that indicates that the array has no objptrs. A related Array_toArray primitive updates the header of an Array_allocRaw primitive to reveal the objptrs. Between the allocation and the cast, all elements of the array must be uninitialized (setting all objptrs in elements to a bogus non-objptr value (0wx1)).

One motiviation for this primitive is that, in a parallel setting, the uninitialization of an array can be a sequential bottleneck. The Array_allocRaw is a constant time operation and the subsequent Array_uninit operations can be performed in parallel.

Note that it is not safe to write non-bogus objptrs into a raw array. Because the raw array's header indicates that the array has no objptrs, non-bogus objptrs written to the array will not be traced or updated during a garbage collection.

In the future, we may consider eliminating Array_alloc (the non-raw array allocation) and moving the uninitialization loop from the runtime to SML. To do so, though, we should ensure that arrays that do not need uninitialization are optimized early in the optimization pipeline.

The extended structure Unsafe.Array has the following signature:

    structure Unsafe.Array:
       sig
          (* omit Size check;
           * objptr(s) at elements set to bogus non-objptr value;
           * non-objptr(s) at elements have indeterminate value
           *)
          val alloc: int -> 'a array
          (* omit Size check;
           * elements set to initial value
           *)
          val create: int * 'a -> 'a array
          (* omit Subscript check *)
          val sub: 'a array * int -> 'a
          val uninitIsNop: 'a array -> bool
          (* omit Subscript check;
           * objptr(s) at element set to bogus non-objptr value
           *)
          val uninit: 'a array * int -> unit
          (* omit Subscript check *)
          val update: 'a array * int * 'a -> unit

          structure Raw:
             sig
                type 'a rawarr

                (* omit Size check;
                 * objptr(s) at elements have indeterminate value;
                 * non-objptr(s) at elements have indeterminate value
                 *)
                val alloc: int -> 'a rawarr
                (* prereq: all objptr(s) at elements set to bogus
                 * non-objptr value (via uninit)
                 *)
                val toArray: 'a rawarr -> 'a array
                val uninitIsNop: 'a rawarr -> bool
                (* omit Subscript check;
                 * objptr(s) at element set to bogus non-objptr value
                 *)
                val uninit: 'a rawarr * int -> unit
             end
       end

@mikerainey

The Array_uninit primitive sets all objptrs in the element at the
given index to a bogus non-objptr value (0wx1).  One motivation for
this primitive is to support space-efficient polymorphic resizeable
arrays.  When shrinking a resizeable array, we would like to "NULL"
out the elements that are no longer part of the logical array, in
order to avoid a (logical) space leak.
It is correct to not special case the Array_uninit primitive, but
doing so inhibits the Useless optimization.
The Array_uninitIsNop primitive answers if the Array_uninit primitive
applied to the same array would be a nop (i.e., if the array has no
objptrs in the elements).  This can be used to skip a
bulk-Array_uninit loop when it is known that the Array_uninit
operations would be nops.

The primitive is translated to a constant by toRssa.

It would be possible to introduce SSA and SSA2 optimizations that
simplified Array_uninitIsNop and Array_uninit when it is clear that
the array will never have objptrs in the elements.  In particular,
while an `(int64 * int64) array` (in SSA) might become an `int64
array` (due to Useless), the reverse can never happen.  Similarly,
while an `((int64 * int64)) array` (in SSA2) might become an `(int64 *
int64) array` (due to DeepFlatten), the reverse can never happen.

Also, RSSA shrink does not seem to simplify the

    switch {test = 0x0: Word32, default = None, cases = ((0x0, L_1277), (0x1, L_1276))}

that results from replacing Array_uninitIsNop with a constant.
The `Array_allocRaw` primitive allocates an array, but with a header
that indicates that the array has no objptrs.  A related
`Array_toArray` primitive updates the header of an `Array_allocRaw`
primitive to reveal the objptrs.  Between the allocation and the cast,
all elements of the array must be uninitialized (setting all objptrs
in elements to a bogus non-objptr value (0wx1)).

One motiviation for this primitive is that, in a parallel setting, the
uninitialization of an array can be a sequential bottleneck.  The
`Array_allocRaw` is a constant time operation and the subsequent
`Array_uninit` operations can be performed in parallel.

Note that it is not safe to write non-bogus objptrs into a raw array.
Because the raw array's header indicates that the array has no
objptrs, non-bogus objptrs written to the array will not be traced or
updated during a garbage collection.

In the future, we may consider eliminating `Array_alloc` (the non-raw
array allocation) and moving the uninitialization loop from the
runtime to SML.  To do so, though, we should ensure that arrays that
do not need uninitialization are optimized early in the optimization
pipeline.
Currently, no arrays are globalized; see MLton#206.

However, if "small" arrays were globalized, then an
`Array_allocRaw[t] (l)` would be globalized as `Array_alloc[t] (l)`
and the `Array_toArray` (that casts the raw array to a normal array)
would be dropped.

Now, the `Array` abstract value tracks whether the array is raw or
normal, which is used to generate either `Array_allocRaw` or
`Array_alloc` if the array is globalized.

Also, `Array_toArray` creates the destination `Array` abstract value
with `Birth.unknown()`, which prohibits globalization.
ShareZeroVec only optimizes

    val a: t array = Array_alloc[t] (l)
    ...
    val v: t vector = Array_toVector[t] (a)

not

    val r: t array = Array_allocRaw[t] (l)
    ...
    val a: t vector = Array_toArray[t] (r)
    ...
    val v: t vector = Array_toVector[t] (a)

The latter is possibly, just requires a more sophisticated analysis to
connect Array_alloc/Array_allocRaw with Array_toVector.
@MatthewFluet MatthewFluet merged commit ca796af into MLton:master Oct 21, 2017
@MatthewFluet MatthewFluet deleted the array-uninit branch October 21, 2017 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant