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 between similar types with different scales it will do the minimum amount 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 C, typename TAG>
class Scalar A quantized integral with a distinct type.
- Template Parameters:
SCALE – Scaling factor.
C – Base storage type.
TAG – Distinguishing type tag.
-
type COUNTER
Imported template parameter C.
The scaling factor SCALE must be an positive integer. Values for an instance will always be an integral multiple of SCALE. The value of an instance will always be a multiple of SCALE.
C must be an integral type. An instance of this type is used to hold the internal count.It can be omitted and will default to
int
. The size of an instance is the same size as C and an instance of that type can be replaced with aScalar
of that size without changing the memory layout.TAG must be a type. It can be omitted and will default to
tag::generic
. TAG is a mechanism for preventing accidental cross assignments. Assignment of any sort from aScalar
instance to another instance with a different TAG is a compile time error. If this isn’t useful TAG can be omitted and will default totag::generic
which will enable all instances to interoperate.The type used for TAG can be defined in name only.:
struct YoureIt; // no other information about YoureIt is required. typedef Scalar<100, int, YoureIt> HectoTouch; // how many hundreds of touches.
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.
Assignment
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
anddec
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 closely
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
desirable 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 sizes 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 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 arithmetic 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”).