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.
Monday, November 17, 2014
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.
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.
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.
Subscribe to:
Posts (Atom)