TS.Scalar

TS.Scalar is a header only library that provides scaled and typed numerical values. Using TS.Scalar starts with defining types which have a scale factor and optionally a tag. Values in an instance of TS.Scalar are always multiples of the scale factor.

The tag is used to create categories of related types, the same underlying “metric” at different scales. To enforce this TS.Scalar does not allow assignment between instances with different tags. If this is not important the tag can be omitted and a default generic one will be used, thereby allowing arbitrary assignments.

TS.Scalar is designed to be fast and efficient. When converting bewteen similar types with different scales it will do the minimum amout of work while minimizing the risk of integer overflow. Instances have the same memory footprint as the underlying integer storage type. It is intended to replace lengthy and error prone hand optimizations used to handle related values of different scales.

Definition

TS.Scalar consists primarily of the template class Scalar. Instances of Scalar hold a count and represent a value which is the count multiplied by SCALE. Note this quantizes the values that can be represented by an instance.

template<intmax_t SCALE, typename COUNTER, typename TAG>
class Scalar
intmax_t SCALE

The scaling factor for the type. This must be an positive integer. Values for an instance will always be an integral multiple of SCALE.

type COUNTER

This is the type used to hold the internal count. This must be an integral type. It can be omitted and will default to int. The size of an instance is the same size as COUNTER.

type TAG

A distinguishing tag which must be a type. It can be omitted and will default to tag::generic. This means that Scalar types without a tag will all interoperate. The tag type is usually a struct defined in name only.

struct YoureIt; // no other information about YoureIt is required.
typedef Scalar<100, int, YoureIt> HectoTouch; // how many hundreds of touches.

TAG s a mechanism for preventing accidental cross assignments. Assignment of any sort from a Scalar to another with a different TAG is a compile time error. If this isn’t useful the TAG can be omitted and will default to the same for all derived types.

COUNTER count() const

Return the internal count.

Scalar &assign(COUNTER c)

Set the count to c.

Usage

In normal use a scalar evaluates to its value rather than its count. The goal is to provide an instance that appears to store unscaled values in a quantized way. The count is accessible if needed.

Assigment

Assigning values to, from, and between Scalar instances is usually straightforward with a few simple rules.

  • The Scalar::assign() is used to directly assign a count.
  • The increment and decrement operators, and the inc and dec methods, operate on the count.
  • All other contexts use the value.
  • The assignment operator will scale if this can be done without loss, otherwise it is a compile error.
  • Untyped integer values are treated as having a SCALE of 1.

If the assignment of one scalar to another is not lossless (e.g. the left hand side of the assignment has a large scale than the right hand side) one of the two following free functions must be used to indicate how to handle the loss.

unspecified_type round_up(Scalar v)

Return a wrapper that indicates v should be rounded up as needed.

unspecified_type round_down(Scalar vs)

Return a wrapper that indicates v should be rounded down as needed.

To illustrate, suppose there were the definitions

typedef Scalar<10> deka;
typedef Scalar<100> hecto;

An assignment of a hecto to a deka is implicit as the scaling is lossless.

hecto a(17);
deka b;
b = a; // compiles.

The opposite is not implicit because the value of a deka can be one not representable by a hecto. In such a case it would have to be rounded, either up or down.

b.assign(143); // b gets the value 1430
a = b; // compile error
a = round_up(b); // a has count 15 and value 1500

round_up and round_down can also be used with basic integers.

unspecified_type round_down(intmax_t)
unspecified_type round_up(intmax_t)

Note this is very different from using Scalar::assign(). The latter sets the count of the scalar instance. round_up and round_down set the value of the scalar, dividing the provided value by the scale to set the count to make the value match the assignment as closesly as possible.

a = round_down(2480); // a has count 24, value 2400.

Arithmetic

Arithmetic with scalars is based on the idea that a scalar represents its value. This value retains the scalar type for conversion checking but otherwise acts as the value. This makes using scalar instances as integral arguments to other functions simple. For instance consider following the definition.

struct SerializedData { ...};
using Sector = Scalar<512>;

To allocate a buffer large enough for a SerializedData that is also a multiple of a sector would be

Sector n = round_up(sizeof(serialized_data));
void* buffer = malloc(n);

Or more directly

void* buffer = malloc(Sector(round_up(sizeof(serialized_data))));

TS.Scalar is designed to be easy to use but when using multiple scales simultaneously, especially in the same expression, the computed type can be surprising. The best approach is to be explicit - a TS.Scalar instance is very inexpensive to create (at most 1 integer copy) therefore subexpressions can easily be forced to a specific scale by constructing the appropriate scalar with round_up or round_down of the subexpression. Or, define a unit scale type and convert to that as the common type before converting the result to the desired scale.

Advanced Features

TS.Scalar has a few advanced features which are not usually needed and for which usable defaults are provided. This is not always the case and therefore access to the machinery is provided.

I/O

When a scalar is printed it prints out as its value, not count. For a family of scalars it can be desireable to have the type printed along with the value. This can be done by adding a member named label to the tag type of the scalar. If the label member can be provided to an I/O stream then it will be after the value of the scalar. Otherwise it is ignored. An example can be found in the <Bytes>_ section of the example usage.

Examples

The expected common use of TS.Scalar is to create a family of scalars representing the same underlying unit of measure, differing only in scale. The standard example of this is computer memory sizezs which have this property and are quite frequently used in Traffic Server.

Bytes

The initial use of Scalar will be in the cache component. This has already been tested in some experimental work which will in time be blended back in to the main codebase. The use will be to represent different amounts of data, in memory and on disk.

namespace tag { struct bytes { static const char * label = " bytes"; }; }
typedef Scalar<1, off_t, tag::bytes> Bytes;
typedef Scalar<512, off_t, tag::bytes> CacheDiskBlocks;
typedef Scalar<1024, off_t, tag::byteds> KB;
typedef Scalar<8 * KB::SCALE, off_t, tag::bytes> CacheStoreBlocks;
typedef Scalar<1024 * KB::SCALE, off_t, tag::bytes> MB;
typedef Scalar<128 * MB::SCALE, off_t, tag::bytes> CacheStripeBlocks;
typedef Scalar<1024 * MB::SCALE, off_t, tag::bytes> GB;
typedef Scalar<1024 * GB::SCALE, off_t, tag::bytes> TB

This collection of types represents the data size units of interest to the cache and therefore enables to code to be much clearer about the units and to avoid errors in converting from one to another.

A common task is to add sizes together and round up to a multiple of some fixed size. One example is the stripe header data, which is stored as a multiple of 8192 bytes, that is a number of CacheStripeBlocks. That can be done with

header->len = round_up(sizeof(CacheStripMeta) + segment_count * sizeof(SegmentFreeListHead));

vs. the original code with magic constants and the hope that the value is scaled as you think it is.

header->len = (((sizeof(CacheStripeMeta) + header->segments * sizeof(SegmentFreeListHead)) >> STORE_BLOCK_SHIFT) << STORE_BLOCK_SHIFT)

Esoteric Uses

Scalar type can be useful even with only temporaries. For instance, to round to the nearest 100.

int round_100(int y) { return Scalar<100>(round_up(y)); }

This could also be done in line just as well avoiding the boiler plate logic.

Design Notes

The semantics of arithmetic were the most vexing issue in building this library. The ultimate problem is that addition to the count and to the value are both reasonable and common operations and different users may well have different expectations of which is more “natural” when operating with a scalar and a raw numeric value. In practice my conclusion was that even before feeling “natural” the library should avoid surprise. Therefore the ambiguity of arithmetic with non-scalars was avoided by not permitting those operations, even though it can mean a bit more work on the part of the library user. The increment / decrement and compound assignment operators were judged sufficient similar to pointer arithmetic to be unsurprising in this context. This was further influenced by the fact that, in general, these operators are useless in the value context. E.g. if a scalar has a scale greater than 1 (the common case) then increment and decrement of the value is always a null operation. Once those operators are used on the count is is least surprising that the compound operators act in the same way. The next step, to arithmetic operators, is not so clear and so those require explicit scale indicators, such as round_down or explicit constructors. It was a design goal to avoid, as much as possible, the requirement that the library user keep track of the scale of specific variables. This has proved very useful in practice, but at the same time when doing arithmentic is is almost always the case that either the values are both scalars (making the arithmetic unambiguous) or the scale of the literal is known (e.g., “add 6 kilobytes”).