Thursday, July 30, 2015

Propsal for new names of integer types

Propsal for new names of integer types:

Although the C standard presently defines integer types whose size is unaffected by the size of int (e.g. uint16_t), the behavior of such types is very much dependent upon the size of int.  For example, in the function

    int32_t comp(uint32_t x, int16_t y) { return x > y; }

machines where int is larger than 32 bits will define uint32_t as a type which will get promoted to a larger signed int, while machines where int is 32 bits or smaller will define uint32_t as a type which will not get promoted except when necessary to balance something of a larger type.

I would propose naming new types wrapN_t and unumN_t with the semantics that if type wrapN_t is defined, its behavior must be essentially equivalent to how a uintN_t would behave on a system where int was no larger than N bits, and if type unumN_t is defined, its behavior must be essentially equivalent to how a uintN_t would behave on a system where int is 2N bits or larger.  Systems which presently define various uintN_t types could define the larger uwrapN_t and the smaller unumN_t types as aliases for uintN_t.  Having code which relies upon promotion behavior use such types in preference to uintN_t would make the intention of the code clearer and would also make it easier to migrate such code to machines with different size integers.  Had the above function used type uwrap32_t or unum32_t instead of uint32_t, the intended meaning would have been unambiguous.

To facilitate migration of code using uwrapN_t to platforms with larger integer types, I would propose the following rules for platforms making them available for types smaller than int:

1. When promoting values to int, the system must keep track of what type they would have been if the size of "int" were equal to that of the smallest supported uwrapN_t type.

2. If one operand of an operator requiring balancing is a uwrapN_t type and the other would have been a type that would have been no larger if int were N bits, then the other operand will be coerced to uwrapN_t, as will be the result of such expression, and behavior shall be equivalent to performing the computation with the uwrapN_t [a system may promote uwrapN_t to int, perform the computation, and then cast the result back, if and only if its behavior in case of any possible overflows would be consistent with having performed the operation directly on the uwrapN_t].

3. Types uintN_t and uwrapN_t must have identical representations, and uwrapN_t and uintN_t shall be considered equivalent in function declarations; it shall be legal for a function declaration to specify any type equivalent to uintN_t and its definition to specify uwrapN_t, or vice versa, even if they are different types.

4. For purposes of the Strict Aliasing Rule, a uwrapN_t shall be equivalent to a uintN_t.

To facilitate migration of code using unumN_t to platforms having smaller integer types, I would propose the following rules for platforms making them available for types whose values are not all representable as "int":

1. When Usual Arithmetic Promotions would apply, a variable of type unumN_t must be promoted to a signed integer type which is capable of representing any value it might hold.

2. If a variable or directly-cast expression of type unumN_t is passed to a variadic function, it shall be equivalent to `uintN_t`.

3. If an expression containing `uNum_t` is passed to a variadic function, and if the promotion required by #1 would cause the result to be a type incompatible from what would have resulted without the promotion, a compiler must reject the code.  A compiler may accept the code if the type is changed but it can ensure compatible behavior.

4. Types uintN_t and unumN_t must have identical representations, and unumN_t and uintN_t shall be considered equivalent in function declarations; it shall be legal for a function declaration to specify any type equivalent to uintN_t and its definition to specify unumN_t, even if they are different types.

5. For purposes of the Strict Aliasing Rule, a unumN_t shall be equivalent to a uintN_t.

Additional proposed types to facilitate optimization

At present, operations on signed integer types smaller than int are subject to different rules from those on larger types, rules which presently impede optimizations.  Further, the guaranteed semantics of unsigned types often limit optimization opportunities even in cases where programmers have no need of those guarantees.  I would therefore suggest that for types intN_t and uintN_t there be additional defined types xuintN_t, ointN_t, and ouintN_t, whose defined behaviors would be consistent with those of intN_t and uintN_t, but subject to the following constraints:

The extensible types:

1. Values and variables of type xuintN_t would be similar to `uintN_t` except that a compiler would be allowed to retain an arbitrary number of additional bits.  If a value of X is stored to e.g. an xuint16_t, each read of that value could yield X & (65535 | arbitrary_value), with a potentially different arbitrary value used for each read.

2. Writing to any of the constituent char or unsigned char values of an extensible-type variable would clear any "extra" bits associated therewith.

3. It shall be legal for a function declaration to specify any type equivalent to uintN_t and its definition to specify xuintN_t, even if they are different types, but the reverse is not true; compilers may flag an error if a declaration specifies xuintN_t and the definition specifies anything other than xuintN_t or ouintN_t, even if the types are otherwise compatible, and must flag an error if it cannot ensure that the function will not receive a value outside the range of its defined type.  Note that systems which declare `xuintN_t` as a synonym for `uintN_t` will implicitly meet this requirement.

4. For purposes of the Strict Aliasing Rule, types uintN_t and xuintN_t would generally be equivalent, with the proviso that writing as a non-x (and non-o) type must cause any "extra bits" to be cleared, and reading as a non-x (and non-o) type must cause any extra bits to be ignored.  Note that systems which declare ointN_t and ouintN_t as a synonyms for intN_t and uintN_t will implicitly meet this requirement.

The optimizer-friendly types:

1. Variables and values of type ointN_t or ouintN_t behave like intN_t and uintN_t with values that are within the range of the latter types, but coercion of out-of-range values to `ointN_t` or `ouintN_t` would be exempt from the normal rules applicable to out-of-range coercions to `intN_t` and `uintN_t`.

2. It is recommended that coercion of out-of-range value to an ointN_t or ouintN_t yield a Partially-Indeterminate Value as described below; such semantics should allow nearly all of the useful optimization opportunities that would be afforded by Undefined Behavior, without requiring that code expend effort to avoid such behavior in cases where a Partially-Indeterminate Value would satisfy program requirements.

3. It shall be legal for a function declaration to specify any type equivalent to uintN_t or xuintN_t and its definition to specify ouintN_t, even if they are different types, but the reverse is not true; compilers may flag an error if a declaration specifies ouintN_t and the definition specifies anything other than ouintN_t, even if the types are otherwise compatible, and must flag an error if it cannot ensure that the function will not receive a value outside the range of its defined type.  Note that systems which declare ointN_t and ouintN_t as a synonyms for intN_t and uintN_t will implicitly meet this requirement.

4. For purposes of the Strict Aliasing Rule, types uintN_t and xuintN_t would generally be equivalent, with the proviso that writing as a non-o (and non-x) type must cause any "extra bits" to be cleared, and reading as a non-o (and non-x) type must cause any extra bits to be ignored.  Note that systems which declare ointN_t and ouintN_t as a synonyms for intN_t and uintN_t will implicitly meet this requirement.

Partially-Indeterminate Value:

A compiler shall be allowed to behave as though each value has a "hidden" bit indicating whether it represents a Partially-Indeterminate Value.  If a value of an N-bit type holds a partially-indeterminate value, then each rvalue conversion may independently behave as any positive or negative mathematical integer which is congruent to the original mod 2^n, without regard for whether that number would be representable in any data type, and without regard for whether the type is signed or unsigned.  If a partially-indeterminate value of a particular type is coerced to a longer type, the value of any additional bits in the result shall be Unspecified; if the longer type can accommodate Partially-Indeterminate Values, the result will be a Partially-Indeterminate Value of that longer type.

For example, consider "ouint8_t x=258; ouint16_t y=x; uint32_t z1=y,z2=y;"

x will be a Partially-Indeterminate Value congruent to 2 mod 256.

y will be a Partially-Indeterminate Value congruent to K*256+2 mod 65536, for some value K

z1 will be equal L1*65536+K*256+2 mod 4294967296, for the above value K and some arbitrary value L1.

z2 will be equal L2*65536+K*256+2 mod 4294967296, for the above value K and some arbitrary value L2not necessarily equal to L1.

Note that under these rules, Partially-Indeterminate Values are highly "contagious", thus facilitating many kinds of optimization, but their behavior is sufficiently constrained that code need not waste time on checks to avoid operations that would yield Partially-Indeterminate Value any possible result from such operations would be acceptable.  As a simple example, if a platform specified that integer overflow would yield Partially-Indeterminate Value, compilers would be able to assume that any integer i will be less than i+1, but code like "uint16_t x,y,z; ... x=y*z;" would have fully-defined behavior for all values of y and z even on a 32-bit system [the computation y*z might yield Partially-Indeterminate Value, but the assignment to x would throw away all indeterminate bits].

Monday, November 17, 2014

New integer types and promotion rules:

New types:

  __ring(1) etc. up to __ring(64)

Ring types would be specified to behave like abstract algebraic ring of integers congruent size 2^n; such types would behave similarly to the way unsigned types of the same size would behave, except for the following:

1. They would be exempt from integer promotions.  Applying binary operators to a ring of any size and an integer or whole-number type of any size would cause the integer to be converted to the ring type, and would yield a result of the original ring type.  Operations involving two different ring types would be forbidden, except for "&" which would yield a result of the *smaller* ring type.

2. Compilers would be required to allow rings of any size up to the implementation's maximum size, regardless of whether the requested size coincides with any multiple of the machine's word size, and include whatever bit-masking operations are required to yield semantically-correct results.  It would be perfectly acceptable for a compiler to allocate a 32-bit storage word for a __ring(3), but all computations on such rings must yield a result in the range 0-7.

  __whole(1) up to __whole(63)

Whole-number types would be specified to behave like integer types capable of holding values from 0 to 2^n-1; any such types smaller than __int would have computations on them performed using using intermediate values of type __int.  As with ring types, the compiler would be free to allocate as much or as little space to each number as it sees fit.

  __signed(2) up to __signed(64)

Signed-number types would hold values from (2^(n-1)) to (2^(n-1))-1; any such types smaller than __int would have computations on them performed using using intermediate values of type __int.  As with ring types, the compiler would be free to allocate as little or as much space to each number as it sees fit.

Overflow rules:

If a numeric overflow occurs, or an attempt is made to store a value into a storage location (other than a ring) which is too small to hold it, the generated code may cause an immediate trap, or may "store" the value, with the constraint that any future attempt to reading the storage location may arbitrarily trap or yield any number which would be correct modulo 2^(n-1), where n is the specified size of the storage location.

Note that Overflow would no longer constitute "undefined behavior"; among the consequences of this, compilers would no longer be allowed to assume that values upon which computations will be performed cannot cause overflow, but would still be allowed to assume that computations will not yield a result that would be arithmetically impossible.  For example, given:

    int64_t test(int64_t x)
    {
      int temp=(x > 3037000500);
      int64_t y =x*x;
      if (temp) return 1;
      if (y > -3) return 2;      return y;
    }

Under C14 rules, a compiler would be allowed to unconditionally set "temp" to zero, since the only way its value would be non-zero would be if the multiplication was going to overflow and allow the compiler to do whatever it felt like.  Under the proposed rules, such treatment would be forbidden.  The test for "y > -3" would be another story, however; a compiler would be allowed to assume that because the arithmetically-accurately-computed square of any number cannot be negative, there is no way y can be negative and thus no need to test for it.  If an overflow occurs, it would thus be possible for the method to return a negative number.



8086 segmentation--One of the great underappreciated ideas of the 1980's

One of the most widely "noted" features of the 8086 architecture is its unusual addressing scheme.  Rather than using a 16-bit linear address and relying upon external hardware to access more than 64K of storage, the 8086 forms physical addresses by taking a 16-bit segment register, shifting it left 4 bits, and adding that to a 16-bit displacement to yield a 20-bit address which can access a 1MB address space.  This design was widely criticized at the time, but in it worked better than any alternatives that existed at the time other than a linear 32-bit address space (which would of course required a 32-bit architecture).  Further, if one compares the IBM PC with the Macintosh, one observes that while PC code often limited things to 64K, the Macintosh (which used a 68000) had a lot of 32K limits.  Even if a processor has 32-bit linear addressing, 32-bit pointers take more space than using 16-bit offsets.

The 8086 architecture was hardly perfect, but the problems for which people blame the segmented architecture were not problems with the concept, but rather were small issues with its implementation.  The segmented architecture would have been great but for the fact that there was only one "general-purpose" segment register.  Even though the processor had four registers which could be used for address generation (BX, BP, SI, and DI), and had a segment register dedicated for use with one of them (SS was dedicated BP), there was only one segment register which did not generally have a fixed place it was expected to point (ES).  Consequently, code that tried to use more than one arbitrarily-placed object at a time would have to reload ES every time it accessed a different object, largely negating the benefits of having multiple address-related registers.  Code which was written with segmentation in mind--especially if it was written in assembly language, could work around this limitation various ways (e.g. by using buffers that were placed near the code, and could thus be accessed using the CS register).  Having even one more segment register (so that SI and DI could access different things while DS continued to access the "main" data segment) would have been a huge improvement.

The other thing that would have improved the usability of segment registers would have been if programming languages were to better recognize different kinds of pointers.  I dislike Microsoft's "memory models", and would have preferred that common practice classify pointers into categories:

  -- 16 bit pointer to something in main data segment (which MS supports)
  -- 16 bit pointer to something known to be on stack (not supported)
  -- 16 bit pointer to start of paragraph-aligned object (not supported)
  -- 32 bit pointer to anything

Having calloc round all allocations to multiples of 16 bytes might seem like it would waste space, but it really wouldn't waste much if memory-allocation-list entries were kept in the slack space.  Further, it would reduce the size of every reference to a calloc'ed object by two bytes; since every such object will have at least one reference to it, and will often have more, that could easily become a major win.

Interestingly, today's .NET and Java frameworks would both work very nicely on a system that combined 32-bit segment registers with 32-bit offsets, even if the system did nothing more complicated than what the 8086 did (but with 32-bit values).  Such a system could easily accesss 64GB rather than 4GB, while retaining the use of 32-bit object references; the only "penalty" would be that object sizes would be rounded up to the next multiple of 16 rather than the next multiple of 4 or 8.  If one wanted to reduce that penalty while allowing more than 64GB of storage, one could have the top 4 or so bits of the segment register select a region of memory while the remaining bits are shifted by a region-specific factor and added to the offset.  Objects up to e.g. 256 bytes could be allocated in an area which is 4-byte aligned; those up to 5105 bytes could be allocated in an area which is 16-byte aligned; those up to 80K in an area that's 256-byte aligned, and huge objects in an area that's 4096-byte aligned.  Using 15 of the 16 segment regions, one could accommodate all of the following

 -- Small objects totalling up to 8 gigabytes (segment 0xxx...)
 -- Medium objects totalling up to 16 gigabytes (segment 10xx...)
 -- Large objects totaling up to 128 gigabytes (segment 110x...)
 -- Huge objects totaling up to one terabyte (segment 1110...)
 -- Additional address space of 256 million segments remaining for other purposes (the stack, OS buffers, etc.)

All while using 32-bit object references.  Small objects would have the same level of overhead they do in 32-bit .NET; larger objects would have slightly more overhead due to alignment, but no more 5%.  Pointers (as opposed to offset registers) would be 8 bytes rather than four, but since code in Java and .NET mainly persists object references rather than pointers, the cost of expanding pointers to 64 bits would be far less than the cost of expanding object references likewise.

Note that the thresholds between object sizes could be adjusted if need be based upon program requirements; the garbage-collector could even perform such rebalancing during program execution.  While there are some programs which would truly need 64-bit object references, I would expect that the extremely vast majority of programs could get by with 32-bit object references using a scheme like the above.

Language philosophy

When designing a language, what goal should drive decisions about when the compiler should automatically apply certain transformations to code (such as adding typecasts) and when it should require that the programmer specify what is desired?

I would posit that a transform should be considered helpful if the meaning of the code with the transform applied would almost certainly match what the programmer intended.  It should be considered harmful in cases where it causes the compiler to accept code whose resultant meaning is likely to be something other than what was intended.


Wednesday, August 13, 2014

Test of links etc.

This is a test.  How will this work?  What can I do with this here?

G C 545 This is a test of an Alert.

Tuesday, August 12, 2014

This is a little script piece which should access the main script. That was a little script piece.

Tuesday, September 3, 2013

Axioms of Implicit Type Conversion

Both Java and .NET publish guidelines about when types should define conversion to various other types. Unfortunately, the actual combinations of implicit type conversions allowed by the frameworks themselves don't seem to have followed any clear systematic rationale.  The fundamental cause of this, I suspect, stems from the requirement that implicit type conversions which allowable in any context must generally be allowable in every conceivable context, when there are at least three different kinds of conversions which would make sense as implicit conversions in different contexts.  There are many axioms which should be applicable to all implicit conversions, but unless one wants to forbid all implicit conversions among floating-point types in all circumstances, the only way to make important axioms hold is to allow implicit floating-point conversions in some circumstances while disallowing them in others.

I would posit that four of the most important axioms related to implicit conversion would be:
  1. If x is some abstract quantity, Tx is the T which represents it best (not necessarily exactly), and T is implicitly convertible to U, then "(U)Tx" should yield either the U which best represents that same abstract quantity, or something essentially as good.  The implicit conversion from "float->double" violates this axiom; conversions from "Decimal->double" or "double->float" would not
  2. For any instance Tx of type T, if T is implicitly convertible to U and U to V, then (V)(U)Tx should yield (V)Tx.  This axiom fails for e.g. long->float->double.  If implicit float->double were eliminated, this axiom could apply even if "Decimal->double" and "double->float" were added [though the addition of other types which were convertible to "double" might violate it].  Note also that proper behavior of this axiom would require that conversion from longer types directly to float be performed as though the value were first cast to double. This would mean that (float)someLong might not yield the float which was closest to someLong, but the difference between that and the closest float would be negligible by "float" standards.
  3. Given instances Tx and Ty of any type T, and any type U, the expression "((U)Tx == Ty) == (Tx == Ty)" should refuse to compile, throw an exception in the typecast to U, or else return true.  All floating-point conversions will violate this axiom if the operands of "==" are implicitly convertible.
  4. Given any mix of variables of any types, if "a==b", "b==c", and "a==c" all compile, and if any two are true, the third must be as well.  The implicit conversions from "int->float" and "long->double" violate this axiom, as does (for .NET) the implicit conversion from `char[]` to `String`.  This is the most significant of the axioms which would be violated by "double->float".
I suspect that the motivation for allowing implicit float->double while disallowing double->float was the fact that the former is compatible with axiom #4 while the latter is not.  I would posit, however, that the proper way to make axiom #4 hold is not to disallow implicit double->float conversions everywhere, but rather to recognize that there are some contexts (such as the "==" operator) where no implicit conversions involving floating-point numbers should be allowed.  Disallowing mismatched operands to "==" would satisfy axiom #3 and #4.  Conversely, allowing "==" to engage in any implicit conversion whose reverse conversion is not reversible violate #3.

What makes the decision to choose conversion rules based upon axiom #4 even more unfortunate is that its main effect is to help the compiler disambiguate code which the compiler should regard as ambiguous.  It's far from clear whether a programmer who writes "someFloat == someDouble" wants to check whether "someFloat" is the best "float" representation of "someDouble", or whether "someDouble" is the best "double" representation of the nominal value of "someFloat" (which may or may not bear much relation to the number that float is supposed to represent).  It's true that only the latter test implements an equivalence relation, but that doesn't imply that it's what the programmer's actually trying to test.

The combination of allowing implicit float->double but forbidding double->float means that code gets cluttered with lots of typecasts in cases where the behavior of the code in the absence of typecasts would be clearer than its behavior with, and yet code which is almost certainly wrong compiles without complaint.  Consider, for example, which of these is more worthy of a compiler warning:

    float oneTenthFloat = 1.0/10.0; // Assume this compiled as though the typecast were present
    double oneTenthDouble =1.0f/10.0f;

The obvious intention of the the first statement would be to set oneTenthFloat to the "float" value which is closest to 1/10, i.e. 13,421,773/134,217,728.  That's precisely what the statement would do if implicit casts were legal in such case, and there would be no plausible reason for the programmer to expect anything else.  The second statement in C would set oneTenthDouble to the "double" value best represents 1/10; it's off by 5.6E-18.  In C# it sets it to a "double" value which is off by 1.4E-9, i.e. more than 268 million times as much.

While there are limits to what can be done to .NET at this stage, there are some changes that could help.  For example, a language could include new compile-time-only types which would be run-time equivalents to the existing primitives, but had different implicit casting behaviors (much the way "Dynamic" is really "Object" under the hood).  Code which as written to use the new types could benefit from the improved semantics and diagnostics, while existing code would continue to run as it always has.  The types and some of their conversion operators could be tagged to indicate that their use should be forbidden in cases where a value of the original type is passed the nth parameter of a function, and the function has an overload with the appropriate number of parameters whose nth parameter can take either the original type (without regard for whether that overload would fit the types of any other parameters).