8

I've got two values from an unsigned 48bit nanosecond counter, which may wrap.

I need the difference, in nanoseconds, of the two times.

I think I can assume that the readings were taken at roughly the same time, so of the two possible answers I think I'm safe taking the smallest.

They're both stored as uint64_t. Because I don't think I can have 48 bit types.

I'd like to calculate the difference between them, as a signed integer (presumably int64_t), accounting for the wrapping.

so e.g. if I start out with

x=5

y=3

then the result of x-y is 2, and will stay so if I increment both x and y, even as they wrap over the top of the max value 0xffffffffffff

Similarly if x=3, y=5, then x-y is -2, and will stay so whenever x and y are incremented simultaneously.

If I could declare x,y as uint48_t, and the difference as int48_t, then I think

int48_t diff = x - y; 

would just work.

How do I simulate this behaviour with the 64-bit arithmetic I've got available?

(I think any computer this is likely to run on will use 2's complement arithmetic)

P.S. I can probably hack this out, but I wonder if there's a nice neat standard way to do this sort of thing, which the next person to read my code will be able to understand.

P.P.S Also, this code is going to end up in the tightest of tight loops, so something that will compile efficiently would be nice, so that if there has to be a choice, speed trumps readability.

John Lawrence Aspden
  • 17,124
  • 11
  • 67
  • 110
  • 3
    It's been a long time since I've dealt with 2's complement, but I think if you write `uint64_t diff = (x - y) & 0x0000 ffff ffff ffff` then the answer will be correct as if you had 48-bit integers. – Andrew May 24 '17 at 19:26
  • That looks terribly plausible and is nice to read. But it's always going to have 0 top bits, so how could it be a -ve value as an int64_t? – John Lawrence Aspden May 24 '17 at 19:31
  • It wouldn't be a negative `int64_t`, but when the top bit is set it will be a negative "`int48_t`". -1 is 0xff..., so -1 in "`int48_t`" will be represented as 0x0000ff... That would be tricky if you want to say something like `x < 0`, which would have to be rewritten as `x <= (((int64_t)-1) & 0x0000ff...)`. – Andrew May 24 '17 at 19:36
  • sure, so now I need to convert it to int64_t – John Lawrence Aspden May 24 '17 at 19:38
  • 1
    To clarify the question -- it's possible that x was before y, and possible that x was after y, and you want a negative result to indicate the latter? (As opposed to knowing that x was the initial measurement, and y was the final measurement, and the result should be unsigned). – M.M May 25 '17 at 00:08

3 Answers3

5

You can simulate a 48-bit unsigned integer type by just masking off the top 16 bits of a uint64_t after any arithmetic operation. So, for example, to take the difference between those two times, you could do:

uint64_t diff = (after - before) & 0xffffffffffff;

You will get the right value even if the counter wrapped around during the procedure. If the counter didn't wrap around, the masking is not needed but not harmful either.

Now if you want this difference to be recognized as a signed integer by your compiler, you have to sign extend the 48th bit. That means that if the 48th bit is set, the number is negative, and you want to set the 49th through the 64th bit of your 64-bit integer. I think a simple way to do that is:

int64_t diff_signed = (int64_t)(diff << 16) >> 16;

Warning: You should probably test this to make sure it works, and also beware there is implementation-defined behavior when I cast the uint64_t to an int64_t, and I think there is implementation-defined behavior when I shift a signed negative number to the right. I'm sure a C language lawyer could some up with something more robust.

Update: The OP points out that if you combine the operation of taking the difference and doing the sign extension, there is no need for masking. That would look like this:

int64_t diff = (int64_t)(x - y) << 16 >> 16;
David Grayson
  • 84,103
  • 24
  • 152
  • 189
  • oh, that looks nice, let me think about it for a bit (tests under construction...) – John Lawrence Aspden May 24 '17 at 19:43
  • 2
    Another idea I just came up with is to shift your 48-bit unsigned integers to the left by 16 before you do anything with them, so then your readings are all uint64_t numbers and they wrap at the expected place, and you don't have to right-shift any signed numbers ever. – David Grayson May 24 '17 at 19:45
  • Original answer doesn't work, I think we need an arithmetic right shift, but the code above is doing a logical one (gcc, I think the behaviour is undefined in general?). However the new idea looks very plausible! – John Lawrence Aspden May 24 '17 at 20:01
  • 1
    Oops, sorry, it does work in gcc, I'd got some 0xffffffffff in my tests. But I think it only works because gcc does the sane thing. However it's probably good enough! We're fairly wedded to gcc and any other compiler that will do a logical shift right on a signed value can just crawl off and die. – John Lawrence Aspden May 24 '17 at 20:24
  • 1
    After a bit of boiling down and testing: return ((int64_t)((x-y)<<16)>>16); return ((int64_t)((x<<16)-(y<<16)))>>16; both work just fine. You don't need the mask in either case. Would you like to add either or both as an answer? – John Lawrence Aspden May 24 '17 at 22:45
  • I added the first one, I think it's nicer. – David Grayson May 25 '17 at 00:06
3
struct Nanosecond48{
    unsigned long long u48 : 48;
//  int res : 12; // just for clarity, don't need this one really
};

Here we just use the explicit width of the field to be 48 bits and with that (admittedly somewhat awkward) type you live it up to your compiler to properly handle different architectures/platforms/whatnot.

Like the following:

Nanosecond48 u1, u2, overflow;
overflow.u48 = -1L;
u1.u48 = 3;
u2.u48 = 5;
const auto diff = (u2.u48 + (overflow.u48 + 1) - u1.u48) & 0x0000FFFFFFFFFFFF;

Of course in the last statement you can just do the remainder operation with % (overflow.u48 + 1) if you prefer.

YePhIcK
  • 5,816
  • 2
  • 27
  • 52
  • 2
    Note, it's implementation-defined whether bit-fields can be bigger than `unsigned int` – M.M May 25 '17 at 00:12
2

Do you know which was the earlier reading and which was later? If so:

diff = (earlier <= later) ? later - earlier : WRAPVAL - earlier + later;

where WRAPVAL is (1 << 48) is pretty easy to read.

mtrw
  • 34,200
  • 7
  • 63
  • 71
  • No, but I think I can assume that the readings are fairly close, so of the two possible answers I think I can take the smallest. Edited question to clarify. – John Lawrence Aspden May 24 '17 at 19:27
  • @JohnLawrenceAspden Taking the smallest would not work if the counter wrapped in between the two measurements, and it seems like the question was phrased to say that you want the code to be able to handle the counter wrapping in between the two measurements – M.M May 25 '17 at 00:13
  • smallest in absolute value. if the two measurements are close to each other, then one possible difference will be enormous and can be ignored. – John Lawrence Aspden May 25 '17 at 00:18