Fast midpoint between two integers without overflow
lemire.me
lemire.me
101
^ 011
-----
= 110
We added 1 + 1, to get 10. We put down a 0, but didn't carry the 1.The carry is separately calculated using AND:
101
& 011
-----
001 <- carry out from LSB.
Of course the carry is carried so we have to shift it left: 010. We can just add the carry afterward (using regular addition which will propagate additional carries not calculated by the AND, like the one from the second digit.Thus the identity:
(a+b) = (a^b) + 2*(a&b).In practice you'd store them in the next larger integer, so 32 bits for a 17 bit int. If you really want to cut off bits or overflow, you use a cast or modulo.
It seems really weird that the "correct" way to do this calculation is to resort to bit manipulation (which should be an implementation detail IMO).
I'm curious which dynamic language reallocates to store larger integers? All of the dynamic languages that I'm familiar with simply store numbers as doubles, with variable width integers being handled by opt-in types.
Say you start with two Int16’s. Any addition would result in an Int17. Adding a pair of those together would result in an Int18, and so forth.
You’d blow past Int64 in no time.
Not really. You only need to preserve according to the msb of each number. If you are adding 0(Int18)+1(Int18), you don't need 1(Int36) anymore than you need 1(Int18) or even 1(Int1).
No, you’d need an Int19. We were talking about statically typed languages, so you need to decide the type at compile-time. If you add two UInt16’s they could both contain up to 0xFFFF, you need 17 bits to store that answer. Basically, with every addition you need 1 more bit than the largest of the two (types, not values) you are adding together to prevent a potential overflow. It’s even worse for multiplication.
(Note that I'm not too clear how valuable this would be. Just asking why that isn't a valid path.)
No, that's completely false. You don't need an Int19 but an Int1.
class bignum {
private:
digit_t *limbs;
public:
bignum(int val);
~bignum();
bignum operator +(const bignum &rhs);
...
};Not so for loops and accumulator constructs.
What is the width of `i` in:
foo(int count) {
int i = 0;
for (int j = 0; j < count; j++) {
i = i + i;
}
}i = 0 implies that i = 0 + 0 = 0; so the loop doesn’t evolve, and the whole thing can be optimized to just i = 0.
For i=1, the loop simplifies to 2^count and a count-length type, which is the point I think you wanted.
The problem you can then run into is that a mathematical operation can OOM
You need some numeric accident involving a higher power operator like exponentiation.
In TXR Lisp I made exponentiation n-ary: you can do (expt x y z ...).
The associativity is right to left: it means
...
z
y
x
rather than the less useful cumulative exponentiation of the same base: yz...
x
which can easily be obtained as (expt x (* y z ..)).So, anyway, if you apply expt to small list of small operands, you can cons up a big number in rather a hurry.
If it is important to prevent a problem like this, a limit can be imposed on bignums (say, large enough for common cryptography to still work).
I gotta disagree on perl, though, even though it can represent numbers outside of the range of a double, it can't manipulate them without converting them into doubles.
It would be desirable that every expression either produces the mathematically correct result, or a runtime exception.
In many cases it would be easy for the compiler to limit intermediate results to some number of bits (since it knows the maximum allowed range for the final result), but it may be a problem to guarantee this.
That is false; a static language could have bignum integers. E.g. you can easily have a bignum class in C++, which is static.
You can't have a variable-length bignum as an unboxed value type.
"Language with unboxed value types" and "statically typed language" are separate, somewhat related concepts.
As you indicate though there's no need for this to be something the user of the type needs to think about.
The problem in C that you can avoid is not taking into account the destination type of a calculation.
If you have in16 + int16, being assigned, returned or passed to a int32 type, then the calculation can be done in int32.
If the result is going back to an int16 type, then there is no need to go to int32.
In C expressions, the types are almost purely synthesized attributes: what that term means is that the information flows up the abstract syntax tree from the children to the parents. in a = b + c, the type of (b + c) is determined without regard for the parent = node. This is very simple and has the benefit of being not only easy to understand but in some ways referentially transparent: when we move (b + c) elsewhere, it retains its meaning (except for the part of what happens to the resulting value in the new context). More sophisticated rules can reduce errors though.
By the way, if int has 16 bits (which is rare nowadays), then the calculation will happen in 16 bits. If int has more than 16 bits, then both operands will be promoted to that size before the operation.
if (a < b)
return a + (b - a) / 2;
else return b + (a - b) / 2;
This method is just more efficient (for places where it matters) as it avoids divisions and branches. But for a vast, vast majority of use-case that tiny efficiency gain doesn’t really matter. return a - (a - b) / 2;Which tweet?
min(a,b) + (max(a,b) - min(a,b))>>1
Smalltalk had first class fraction objects as part of it's transcendental number system. There's a great story about Dan Ingalls changing one line of code to make all of the original Smalltalk BitBlt transparently work with fractions. I always miss having fractions as part of the transcendental math experience in "very high level languages".
The downside of these approaches, is that you can optimize some paths so that things stay relatively quick, but other paths will really slow down all of a sudden.
For example, in Smalltalk,
16r7FFFFFFF timesRepeat: [ self fastOp ]
would allow you to do a microbenchmark on a fast operation. But if you moved to 16r7FFFFFFF + 1 timesRepeat: [ self fastOp ]
would suddenly cause your benchmark to take 30x+ longer, because you had tripped from immediate tagged integer format to integer-as-an-object representation.If you wanted it to wrap around, you could use an expression like "a = (a+1) % 256", or maybe something like Ada's modular types.
Exactly, Ada's modular types would be a good option in this case, if that is what you want (my feeling is, most likely not unless you are doing some low level stuff). An alternative would be to rewrite the for loop in a functional or range based style.
In algorithmic code, you almost never want overflow. If you have a little function to calculate something, you want the intermediate variables to be big enough to perform the calculation, and in the end you cast it down to the size needed (maybe the compiler can do it, but maybe you know from some mathematical principles that the number is in a certain range and do it manually). In any case, I would want to be warned by the compiler if I am:
1. loosing precision
2. performing a wrong calculation (overflowing)
3. accidentially loosing performance (using bignums when avoidable)
1 and 2 can happen in C if you are not careful. 3 could theoretically happen in Python I guess, but it handles the transition int <-> bignum transparently good enough so it was never an issue for me.
You're proposing a language where you cannot increment integers? I don't think that would be a very popular language.
a = a &+ 1
or the shorthand a &+= 1
IMO, that looks ugly, but that probably is a matter of getting used to it.Compared to the %256 option, it has the advantage that, if you change the type of a, you won’t have to remember to change the modulus.
They also chose to not make modular integers separate types. That makes mixing ‘normal’ and modular arithmetic on integers easier (with modular types, you’d have to convert between regular and modular integers all the time) (edit: that also is consistent with the fact that the bitwise operators work with regular ints, and ook not require a special “set of 8/16/32/…” type that’s implemented as an integer)
I wouldn’t know how common such mixing is and, hence, whether that is a good choice.
a = b.wrapping_add(c);Are there any languages other than Lisp and Python that have automatic bignum support?
Clojure makes a gesture toward it, literals are automatically promoted but the normal operators don't do it:
user=> (+ 1 9223372036854775807) ;; max signed int64
Execution error (ArithmeticException) at user/eval5 (REPL:1).
integer overflow
user=> (+ 1 9223372036854775808) ;; one higher
9223372036854775809N % expr 1 + 9223372036854775807
9223372036854775808
% expr 9223372036854775808 + 1
9223372036854775809However they don't have JS compatibility either (Dart does not round off large integers like JS does), so I forget what the point was.
Dart also (very early) had infinite precision fractions. So if you divided 355 by 113 you didn't get 3 or 3.1415... you got the fraction 355/133 which was an instance of a first class numeric type.
Unfortunately this means your numbers can grow to need arbitrary amounts of storage in loops.
A cooler feature would be requiring the compiler to prove the addition wouldn’t overflow.
Wuffs is specialized enough to do exactly that (https://github.com/google/wuffs).
for i in [0, 100]:
// i is known to be in [0, 100]
// the argument is known to be in [0, 200]
do_something(i*2)
If you really want unbounded growth, you need a bignum. If you want to be bounded in size but not in time, you have to specify overflow behavior. Something like (ugly pseudocode): integer[modulo 256] a = 0;
while some_condition:
a += 1
or integer[0, 1000] b = 0;
while some_condition:
guard if b < 999:
// b is now in [0, 999]
b += 1
The whole point is, forcing you to make your mind up about overflow behavior (and not just using int32 or int all the time and hoping i is going to be "small").In c/c++ it is. Obviously some other languages would disagree.
I think a better example of what GP is thinking about is Rust's approach, where overflowing an u8 panics (in debug builds), but you can do x.wrapping_add(y), x.saturating_add(y), x.checked_add(y) etc., depending on what you want to happen when the operation overflows.
The compiler could use static analysis to keep the size to a minimum, for example in this case:
int32 a
int32 b = 2
var c = a * b
it would know that c just needs 33 bits.Assuming x and y are 32-bit integers
But it only goes up to 64-bit, then you get overflow again
CppCon 2019: Marshall Clow “std::midpoint? How Hard Could it Be?”
The majority of programmers have no idea what an int is in their favorite language and what its range is (roughly).
Then the majority come up with (a / 2) + (b / 2) until they run the unit tests and realize it's wrong.
And so on and so forth, with this question you can uncover layers upon layers of trivial (non-)knowledge.
IMHE this is the kind of edge that you know only because you've been bitten by a bug once.
It's the same with floating point numbers. You may know that the representation is not absolute, that you can end up with NaN. But I found that I only knew it viscerally after I banged my head on bugs related to these.
Of course, that could be provided by Comp Sci ou Comp Eng curriculum, but time is finite...
In the 5-10% of engineers who saw the problem, how many had experienced it once themselves before?
So you are an Android engineer and you deal with ints a lot. Screen coordinates are ints on Android, so if you think the range of an int is "256" how do you think your app works at all?
This question reveals to me one of the most important things I'm usually looking for when hiring: natural curiosity. A software engineer should be curious about things he or she is dealing with. And that starts with the most trivial things like "what is an int really?" and then moves on to other concepts like: under the hood, what is a function?, what is a virtual method? what does `await` really do? And so on.
A good engineer should know how the computers work, and I don't know why this should be even questioned.
Whether or not this is a completely ludicrous answer depends entirely on how you presented the question (i.e. whether or not it was clear that you're talking about java instead of asking a more general question).
For example, in C, the int type can be as low as 16 bits in size, yielding "65 thousand-something" possible values in the worst case. So that could be a reasonable answer as the guaranteed range of values for an int. And even in an android interview, C(++) can conceivably be the assumed context if the previous questions have been NDK-related.
> Let alone it's not even a range!
I feel like it's not a particularly uncommon shorthand to refer to the extent of a range of values that something can take as "the range" of that something.
Wrong both for worst-case C and for "16 bits in size": the actual maximum is "32 thousand-something" (specifically 32767 in 2s-complement and also in most of the stupid representations (like 1s-complement or sign-magnitude), although there might be some that have, eg, 32768). They also have a minimum of -32768 (or -32767 or otherwise for some of the stupid representations).
You could intepret it as "65 thousand-something" values between the minimum and maximum, but that strongly implies that the minimum doesn't need to be specified, which only works for unsigned integers (which C int is very much not).
"256" is a ridiculously bad answer on multiple levels. Believe me, I heard it from more than one Java developer with a CS degree and at least 5 years of experience at the time.
I am not disputing this point, I agree with it.
I am saying there is a difference between knowing int can overflow or knowing that floating point numbers are imprecise, and being attentive when you read `a + b` or `a == b` with float. I believe only experience can teach that (such experience may or should be provided by school).
Because while it is easy to be bitten by this on at 16 or 32 bits, if it happens at 64 bits (1.8446744e+19) it's almost certainly an abstraction error like arithmetic on identifiers rather than values.
Back around 2010, I wrote some code for the first time in a very long time and that code initialized a 10,000 integer array and my first thought was "that's too big to work." Kilobyte thinking in a gigabyte future.
To a first approximation, as an interview question it fights the last war...again embedded systems excepted.
int avg(int a, int b) {
if ((a < 0) != (b < 0)) // not the same sign?
return (a + b) / 2;
else
return a + (b - a) / 2;
}This will round (-2,-1) to -2, i.e. away from zero. For comparison, if we perform the canonical (a+b)/2 instead it will round to -1, i.e. towards zero.
Now, the problem statement does not tell us how to round, so you’re technically correct, but the inconsistency bothers me.
Edit: never mind, this can overflow too
I'm asking as I doubt "know about how computers work" is necessary for most tech roles.
I get it, having technical expertise is important, but still ...
Is it though? It just tells you if they know the trick or not. At that point you might as well have them fill out a form and use the score from that to hire/no hire.
It doesn't tell you if they understand what RAM is or storage (HDD/SSD/etc) or the various buses on a motherboard or pretty much anything about how a computer works. For the example given in the article, it's pretty rare for (a+b)/2 to overflow since the default ints end up being 32 bit (article calls out 64bit tbh) and your parameters are [-1000,1000].
---
> 90-95% of engineers don't even see a problem with (a + b) / 2 until you tell them about the overflow, let alone find a solution for it.
In my experience a similar percentage can't write a working DFS which I think is much more work related than midpoint.
def is_triangle(a, b, c) -> bool:
...etc...
One of the things an ideal candidate would realize is that the triangle inequality needs to apply (a + b >= c (for all permutations of a,b,c)), and that if a developer naively implemented the above via: if a + b < c:
return false
it'd run into this exact problem.I'd thought this question had gotten stale / overdone, but perhaps it's still a great interview question.
First of all, doesn't it also need to have the condition (c > a - b)? Maybe you just left this part out?
Secondly, you're worried that (a + b) could overflow. The triangles in your applications are just THE MOST EXTREME! That's how cool your application is! You have the MOST EXTREME TRIANGLES!
But wait! When are you dealing with integer lengths of triangles? You never specified they were integers. In 99.99% of all real-world applications dealing with triangles, their coordinates are floating-point. I think it's fair to say overflow isn't nearly your biggest arithmetic problem with floating points -- rather, once you get to extreme (and extremely different) exponent values, you have all sorts of accuracy and loss issues, long before overflow becomes an issue. Do you expect the candidate to also handle the case where one side is 1e-155 and another is 1e274? Because otherwise their code is "wrong"!
So left unspecified, your "gotcha" is completely ridiculous and just a mind-reading trap to arbitrarily filter out people who don't think exactly (and erroneously) like you do!
Or maybe you did mean that the sides of the triangle are constrained to integer lengths? That would be extremely unusual, so you absolutely need to specify that. But if you're constraining the side lengths to integers, are you also constraining the vertex coordinates to integers? It would be extremely strange to require integer lengths but allow floating-point coordinates; but it would also be extremely strange to only allow integer coordinates, as most triangles with integer coordinates have a least one non-integer side length! And it doesn't sound like a trivial problem at all to find whether a given set of integer lengths could form a triangle on an integer lattice, but that seems to be what you maybe think you're asking? Do you even know what you're asking?
If integers are involved at all, it's far more likely that the coordinates are integers, but the side lengths are floating point.
What a tremendously awful interview question. I really hope your description is just extremely limited and flawed and mis-remembered, because if that's the actual question, you are perpetuating the "arbitrary leetcode interviews" problem that competent software engineers always complain about when they look for jobs.
Seems like you can cover the corner case easily enough:
x/2 + y/2 + (x & y & 0x01)
A few more operations than the article though so not the most efficient solution.This returns -1 for x = INT_MIN and y = INT_MAX were the answer should be 0 (for an example). so not a correct solution
The C standard says: When integers are divided, the result of the / operator is the algebraic quotient with any fractional part discarded (This is often called ‘‘truncation toward zero’’).
So it should be 0 (as per C standard, not sure what C++ standard says)
2^64 is 1.8446744e+19
To a first approximation, if your application is overflowing 64 bit integers, the problem is in the abstractions not sloppiness in low level details...something like doing arithmetic on IPv6 addresses.What I mean is that it's one think if you specify a 32-bit or 16-bit architecture because the job entails low level programming with that constraint.
But entirely another think if it is used as a general test of software engineering skill because on the one hand, now I know the trick of the trick question and you wouldn't hire me based on my software engineering chops.
And on the other hand, in the vast majority of cases, the solution that might overflow is not just the simplest thing that might work, but it will also work well enough for the business purpose and be easier to support and maintain in the code base.
Finally, handling problems like this culturally during development and after-action debriefing is better healthier than how-many-golfballs at the interview stage...like I said, I know the answer.
When you write a bit of code, you naturally have in mind the realistic range of values you'll be working with. Even if it's just within 4 orders of magnitude. You know whether you're dealing with thousands or quadrillions. In the extremely rare case it's the latter, then you start worrying about this. You just can't worry about being 10 orders of magnitude off in your assumptions all the time -- that's what Exceptions are for. Holy crap, this software is dealing with values ten orders of magnitude different than you programmed for!? Absolutely throw an exception in that case, because it requires a complete rewrite of that area of the code.
Yes, if you're writing midpoint in a language-standard math library, it should work for all possible inputs. But the point of looking at toy problems in software engineering blogs is to inform us how to write our much more complicated, much more domain-specific code, and these lessons just don't cleanly transfer into that world.
Just to pretend to be an engineer a bit longer, 64 bits still provides a few orders of magnitude of overflow headroom for integer subtraction, addition, and division.
Multiplication is another story and might come up if you’re rolling your own cryptography. But then you have two problems since 64bits isn’t big enough.
Or rather three since you are rolling your own cryptography.
Why is that wrong?
In general, even with floating point numbers you can get different results by rounding implicitly or intentionally the intermediate values versus the end result.
But in computing, assuming we are only dealing with integers, 1/2 == 0. So 1/2 + 1/2 returns 0.
"This will be too low if the original addition contained a carry from bit 0 to bit 1, which happens if bit 0 is set in both of the terms, so we detect that case and make the necessary adjustment."
return (a / 2) + (b / 2) + (a & b & 1);
In Rust you can actually do this using the checked add/sub/etc methods. It's one of the things I really appreciate about the language. By default, it panics on overflow in debug builds. You have to use the wrapping variants to declare that's intentional.
How about:
if a > b {
mid = (a-b)/2
} else {
mid = (b-a)/2
}
There must be a way to do it without the branch?
edit: Yes, in the article, I'm an idiot. if a > b {
mid = (a-b)/2
That only avoids overflow on unsigned types (where it would be called wraparound) ...0: Assuming your language rounds integer division and modulo correctly, ie that `i%array_len` is reliably a valid array index. C has problems here when i (respectively: a or b) is signed, but that doesn't matter in the sensible case, where you always index everything with size_t.
[1]: https://ai.googleblog.com/2006/06/extra-extra-read-all-about...
int mid(int x, int y) {
return (x/2 + y/2) + (1 & x & y);
}
would be a more readable solutionedit: Actually, this fails on mid(INT_MIN, INT_MAX) and possibly other mixed sign values (returns: -1, expected: 0 (or -1 is okay?), where the precise answer is -0.5)
more edit: The C standard says: When integers are divided, the result of the / operator is the algebraic quotient with any fractional part discarded (This is often called ‘‘truncation toward zero’’).
So -1/2 should be 0.
(x >> 1) + (y >> 1) + (x & y & 1)
This rounds toward negative infinity. Also (x >> 1) + (y >> 1) + ((x | y) & 1) // Rounds toward +Inf
(x >> 1) + (y >> 1) + (x & 1) // Rounds up if x is odd
(x >> 1) + (y >> 1) + (y & 1) // Rounds up if y is odd
I'd be curious if there's a bit-twiddling trick for rounding toward x or y.https://en.cppreference.com/w/cpp/numeric/midpoint
Though it's odd this allows overflow.
They were developed by programmers at places like the AI labs at MIT and Stanford, in much earlier days of computing, when size and time constraints sounded much more in everyone's faces than today.
[1]: https://github.com/id-Software/Quake-III-Arena/blob/master/c...
Many past discussions on HN: https://hn.algolia.com/?q=inverse+square+root
When I got into "AI", much later, coming from CS and software engineering thinking, "AI" seemed to be "things we currently think are hard for computers to do (and useful computational methods that we've already figured out)".
Now "AI" is getting different, and generalized AI is looking more plausibly attainable than it was shortly before DL. (Minsky thought it would happen quicker, and also speculated explosive growth of capability once the AI could teach and evolve itself.)
min(x, y)+( ((unsigned int)abs(x-y))>>1);
with no issue.
abs(x-y)is the distance between points x and y. We don't care about order here because of the absolute value. And by its nature, it will always be positive - hence unsigned.
We divide the distance between the points by 2. This always provides a solution that fits in the signed bounds of X and Y once you add to min(x,y).
And it costs an ABS, a subtract, a non-negative bitshift, and a min().
To make it more complete, a switch statement depending on type of input function would be needed to handle the various sizes of numbers. And then it'd be doing the same but for long int->unsigned long int etc.
The subtraction can overflow, e.g. INT_MIN - 1, or 0 - INT_MIN. The abs call can also overflow, with abs(INT_MIN). In both cases, the overflow causes undefined behaviour.
To calculate the difference between 2 signed integers we must bear in mind that the result may exceed INT_MAX, and must use unsigned int for the result. I wrote about this on StackOverflow: https://stackoverflow.com/q/10589559
The second formula is more elaborate, and makes heavy use of the above identity:
2*(a|b)-(a^b)
= 2*((a&b)^a^b)-(a^b) (express boolean or in terms of and and xor)
= 2*(a&b) ^ 2*(a^b) - (a^b)
= 2*(a&b) + 2*(a^b) - (a^b) (there can't be carries, so xor equals addition in this case)
= 2*(a^b) + 4*(a&b) - 2*(a&b) - (a^b)
= 2*((a^b) + 2*(a&b)) - 2*(a&b) - (a^b)
= 2*(a+b) - (a+b)
= a+b.Consider adding two single bits, X and Y - you'll get the following sums, denoted in binary (column CM):
X Y | CM
----+---
0 0 | 00
0 1 | 01
1 0 | 01
1 1 | 10
As you'll note, the M-bit is just XOR and the C-bit is AND (google "half-adder" if you're into hardware).And as we remember from school, the carry (C-bit) always has to be added to the next column to the left, that's why we shift it by one bit (aka multiplication by 2).
However if you're doing mid-point in something like binary search where you already know y >= x AND x >= 0, then x + (y - x) / 2 is indeed a fine choice. It's a good one to remember.
ADD a, b
RCR $1, a
If overflows are allowed, like in Rust, you could implement this by representing X and y with two ints each and doing mini big integer arithmetic on them.
x, y = minmax(x, y)
return x + (y - x) / 2;
? min = x^((y^x)&-(y<x));
return min + ((y^x^min)-min)/2;
Still less efficient. add eax, ebx
rcr eax, 1
ARM: adds r0, r0, r1
rrx r0, r0
I’d need to think a bit more to come up with a signed version.Invert the high bits, turning two's complement into biased format (0 = lowest negative number, 0xFF...F = highest positive number). Then do the add+rotate and convert the result back.
R3 = (r1 + r2) >> 2
R3 = R3 + 0x1000 0000 0000 0000 if carry bit is set.
So should only be 2 instructions.But y>>1 + x>>1 + (x & y & 1) is easier to remember than ((x^y)>>1) + (x&y).
For signed integers, you need to be careful depending on the language you are using. Most languages provide a signed right shift that doesn’t lose the sign.
Personally, I think shift should always have been a bitwise operator, without sign extension. To me signed right shift feels as sensible as right shifting a floating point number - a shift is bitwise and not an arithmetical operator. But I guess that’s what comes from being brought up on healthy machine code by robots in the steel jungle.
C recommendation: “INT13-C. Use bitwise operators only on unsigned operands” https://wiki.sei.cmu.edu/confluence/plugins/servlet/mobile?c...
add x,y
rcr x,1For x=INT_MIN+1 (0b1000...1) y=INT_MAX (0b01111...1) this gives INT_MIN (0b1000...0) instead of 0.
I think in the unfortunate example question the binary search is from -1000 to +1000, but the question is not about binary search, it is about finding the midpoint between two numbers.
That's not true. Binary search doesn't map everything to a positive integer. You are incorrectly looking at the index offset, but the issue is determining the midpoint between two signed numbers.
Let us say that I ask you to find the number I am thinking about between -1000 and 1000, by repeatedly guessing a number. With each guess, I tell you whether your guess is correct, smaller or larger than my number. A binary search algorithm tries to find a value in an interval by repeating finding the midpoint, using smaller and smaller intervals. You might start with 0, then use either -500 or 500 and so forth.
You don't need negative numbers to do this.... you can easily do it with only positive numbers.
Since the expression will be eventually assigned to some variable or passed as a function parameter, it will have a limited range (and there should be an exception if that overflows), but intermediate results could be larger.
Is this just completely unfeasible, or just not done because "that's not how C does it"?
// Calculates the midpoint of two 64-bit integers
// and returns the result as a 64-bit integer.
int64_t midpoint(int64_t a, int64_t b) {
// Use the __int128 type to calculate the sum
// of the two input numbers without overflowing.
__int128 sum = (__int128) a + (__int128) b;
// Shift the sum to the right by 1 bit to divide it by 2
// and get the midpoint. This operation is equivalent to
// sum / 2, but is more efficient because it uses a bit shift
// operation instead of a division operation.
int64_t midpoint = (int64_t) (sum >> 1);
return midpoint;
}My idea was a better language where typecasts are never needed at all, because the compiler knows how the result will be used (in this example, returned as int64_t), and can produce whatever code is most efficient and either produces the correct result or a runtime exception.
edit: Also any non-toy compiler will optimize division by powers of two into a shift operation, so ChatGPT isn't being clever at all here, just repeating a common superstition.
> ChatGPT isn't being clever at all here, just repeating a common superstition
Source:
#include <stdint.h>
int64_t midpoint_ChatGPT(int64_t a, int64_t b) {
__int128 sum = (__int128) a + (__int128) b;
// Shift the sum to the right by 1 bit to divide it by 2
// and get the midpoint. This operation is equivalent to
// sum / 2, but is more efficient because it uses a bit shift
// operation instead of a division operation.
int64_t midpoint = (int64_t) (sum >> 1);
return midpoint;
}
int64_t midpoint_rep_lodsb(int64_t a, int64_t b) {
__int128 sum = (__int128) a + (__int128) b;
// Shifts are for the superstitious.
int64_t midpoint = (int64_t) (sum / 2);
return midpoint;
}
Clang -O2 output: _midpoint_ChatGPT:
pushq %rbp
movq %rsp, %rbp
movq %rdi, %rcx
sarq $63, %rcx
movq %rsi, %rax
sarq $63, %rax
addq %rdi, %rsi
adcq %rcx, %rax
shldq $63, %rsi, %rax
popq %rbp
retq
_midpoint_rep_lodsb:
pushq %rbp
movq %rsp, %rbp
movq %rdi, %rcx
sarq $63, %rcx
movq %rsi, %rax
sarq $63, %rax
addq %rdi, %rsi
adcq %rcx, %rax
movq %rax, %rcx
shrq $63, %rcx
addq %rsi, %rcx
adcq $0, %rax
shldq $63, %rcx, %rax
popq %rbp
retq
GCC -O2 output: midpoint_ChatGPT:
endbr64
movq %rsi, %r8
movq %rdi, %rax
sarq $63, %rdi
sarq $63, %rsi
movq %rdi, %rdx
addq %r8, %rax
adcq %rsi, %rdx
shrdq $1, %rdx, %rax
ret
midpoint_rep_lodsb:
endbr64
movq %rsi, %rcx
movq %rdi, %rax
sarq $63, %rdi
sarq $63, %rcx
movq %rdi, %rdx
addq %rax, %rsi
movq %rcx, %rdi
adcq %rdx, %rdi
xorl %edx, %edx
movq %rdi, %rcx
shrq $63, %rcx
movq %rcx, %rax
addq %rsi, %rax
adcq %rdi, %rdx
shrdq $1, %rdx, %rax
ret midpoint_rep_lodsb_handwritten:
endbr64
movq $0x8000000000000000, %rax
xorq %rax, %rsi ;convert two's complement to biased
xorq %rax, %rdi
addq %rsi, %rdi
rcrq $1, %rdi
xorq %rdi, %rax ;convert back
ret
(sorry if I got the syntax wrong, AT&T is just horrible)