5.8 Streams

Stream objects, which hold sequences of data to be operated upon by programs compiled to a "cpu:stream" or "gpu:stream" compilation target, are represented in Sh using the ShChannel template class and ShStream class.

A channel is a sequence of elements of the type given as its template argument. Channels are an abstraction and the channel data representation is opaque, but channel data can be used as a vertex array input. The Sh runtime attempts to use the most efficient operations and representations available for managing buffers. Updates to data are timestamped to avoid data transfers whenever possible.

Streams are containers for several channels of data, and are specified by combining channels (or other streams) with the “&” operator. Streams only refer to channels, they do not create copies. However, streams may not refer to themselves as components. A channel can still be referenced as a separate object, and can also be referenced by more than one stream at once. For convenience, an ShChannel of any type can also be used directly as a single-channel stream.

Sh uses a reference-counting garbage collection scheme; most Sh types are in fact smart pointers to separate data items. Even if a channel is destroyed (explicitly or implicitly), if a stream refers to this data the memory will not be released.

In addition to being viewed as a sequence of channels, a stream can also be seen as a sequence of homogeneous records, each record being a sequence of elements from each component channel. Stream programs conceptually map an input record type to an output record type. If an ShProgram is compiled using a "stream" compilation target, it can be applied to streams. Stream programs are applied in parallel (conceptually) to all records in the stream.

The connect operator is overloaded to permit the application of stream programs to streams. For instance, a stream program p can be applied to an input stream a and its output directed to an output stream b as follows:


b = p << a;
When specified, the above stream operation will execute immediately, and will return when it is complete (later on we plan to add a retained mode to permit greater optimization). At the point of execution, Sh will check (dynamically) that the input and output types of the program match the types of the input and output streams.

Use of “p << a” alone creates an unevaluated program kernel, which is given the type ShProgram (and can be assigned to a variable of this type, if the user does not want execution to happen immediately). Internally, input attributes are replaced with fetch operators in the intermediate language representation of the program. These fetch operators are initialized to refer to the given stream’s channels. Such program objects can also be interpreted as a “procedural stream”. Only when this unevaluated procedural stream is assigned to an output stream is the kernel executed.

The implementation of the << operator permits currying. If a program object is applied to a stream with an insufficient number of channels, an unevaluated program with fewer inputs is returned. This program requires the remainder of its inputs before it can execute.

In a functional language, currying is usually implemented with deferred execution. Since in a pure functional language values in variables cannot be changed after they are set, this is equivalent to using the value in effect at the point of the curry. However, in an imperative language, we are free to modify the value provided to the curried expression. We could copy the value at the point of the curry, but this would be expensive for stream data. Instead, we use deferred read semantics: later execution of the program will use the value of the stream in effect at the of point actual execution, not the value in effect at the point of the currying. This is useful in practice, as we can create (and optimize) a network of kernels and streams in advance and then execute them iteratively.

The “<<” operator can also be used to apply programs to Sh tuples. A mixture of tuple and stream inputs may be used. In this case, the tuple is interpreted as a stream all of whose elements are the same value. The same by-reference semantics are applied for consistency. In effect, what happens is that an input “varying” attribute is converted into a “uniform” parameter, a useful operation.

Since we provide an operator for turning a varying attribute into a uniform parameter, we also provide an inverse operator for turning a parameter into an attribute. Given program object p and parameter object x, the following removes the dependence of p on x, creating a new program object q:


ShProgram q = p >> x;
The parameter is replaced by a new attribute of the same type, pushed onto the end of the input attribute list.

The “&” operator can also be applied to streams, channels, or tuples on the left hand side of an assignment. This can be used to split apart the output of a kernel. For instance, let a, b, and c be channels or streams, and let x, y, and z be streams, channels, or tuples. Then the following binds a program p to inputs, executes it, and extracts the individual channels of the output:


(a & b & c) = p << x << y << z;
This syntax also permits Sh programs to be used as subroutines (let all of a, b, c, x, y, and z be tuples).


Note: This manual is available as a bound book from AK Peters, including better formatting, in-depth examples, and about 200 pages not available on-line.