TL;DR:
It was a limited solution to bridge a gap in chip development.
There was a small window of usefulness between the time chip technology was capable to support a CPU with pipelines long enough to create a failed prediction penalty (i.e. the wrong sequence fetched) but not providing sophisticated branch prediction units (and data storage) and the time this got solved.
In addition it's only really useful if the penalty for a miss is a very hefty one, like with the extreme long pipeline of a Pentium 4 - which is exactly where Intel introduced the 2E/3E branch hint prefixes. The same that later got dropped when instruction preparation and execution got complete redesigned and pipelines shortened (together with bigger and better branch prediction).
Points to think about:
Hint prefixes will increase code size, which in turn will lower performance overall, as it means fetching an additional byte from memory (and cache), taking up bandwidth (*1).
There will be only a one-time gain as it only pays out if the hint is true and if that branch is only encountered once. If executed a second time, a branch prediction will be as effective.
It carries always maximum penalty in cases where the hint is incorrect.
A BTFN (*2) algorithm will already improve most cases of branches where static hints have a high success chance: loops.
But most importantly:
- Any Branch Prediction will usually outperform static hints.
Implementing branch prediction is not just always a good idea, but a real must to support code without hints (or worse, bad hints). So as soon as there is a branch prediction, branch hints turn into a burden.
So the question might be rather:
Why was branch hinting considered in the first place?
As so often in (chip) development, it's a matter of resources. A hint based system can be implemented very easy with extrem low hardware effort within the fetching logic:
(situation when hitting a hinted branch)
- fetch hint opcode
- fetch the branch
- decode hint (can be done in parallel)
- depending on hint
- continue fetching following instruction, or
- fetch starting with branch target.
(whenever the condition is checked)
- if branch condition test does not support hint
- flush pipeline and start fetching the other way.
When comparing this with the way fetching works without, it becomes obvious that the difference is only in how a (hinted) branch is handled.
An operation is treated as
- a sequential operation without any hint
- a sequential operation if there's a 'usually not taken' hint
- a jump (unconditional branch) if there's a 'usually taken' hint
Any sufficient complex pipeline will already handle both cases accordingly, so it's really just a small addition to switch for either according to the hint. Hardware cost for hinting is very low (*3,*4). Thus an implementation of hinting was easy to achieve at a time before large (and fast) prediction tables became affordable. But as soon as chip real estate provided them, hinting became a (mostly) useless burden.
Practical Use
Last, but for sure not least, a look at use cases for hinting might help as well, as there is essentially only one:
- extreme one sided data sets with known tilt
Which comes in 3 flavors:
- uneven user data
- looping/list processing
- error checks (think
if (read(...) == -1) {...} clauses
)
Of these
- the first is hard to predict
- the second is again rather likely covered by a BTFN
leaving only
- the last as generic use case where a BTFN will usually fail and a hint would be helpful.
Not much of a use case that is.
*1 - This could be mitigated by using 3 sets of branches (taken/not taken/no hint) - of course at cost of code space.
*2 - Back Taken/Forward Not - which means any branch to a lower address (back) is assumed to be taken, while a higher target address is assumed to be not taken.
*3 - A FF for the hint (a few if more complex) plus maybe an address register and a few logic gates. seriously not much to think about if this is already a CPU with pipelining (one without doesn't need hints at all).
*4 - Then again, adding a BTFN logic is as simple while catching quite many cases, although slower as it needs to check the branch target address first.
__builtin_expect
), other languages could do similar if they were interested. x86 gcc uses those hints to emit code that the static branch predictor will do the right thing with (e.g. putting the body of an "unlikely" if behind a forward branch that will be predicted not-taken).