Author Topic: Why is loading a multi byte scalar variable from some address so complicated?  (Read 17211 times)

0 Members and 3 Guests are viewing this topic.

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Quote
Consider a hardware architecture that has an arithmetic shift left, i.e. multiplication by a power of two, that either saturates or wraps around to a positie value.

I have never (significantly) programmed arm32 in assembler but normally CPUs have a shift left with a zero fed in from the right.
That has nothing to do with this.  What I mean is this:

Signed arithmetic shift left with a positive argument:
      0b01111000'00000000 << 1 = 0b01110000'00000000
      0b01110000'00000000 << 1 = 0b01100000'00000000

Signed arithmetic shift left with a negative argument:
      0b11111000'00000000 << 1 = 0b11110000'00000000
      0b11110000'00000000 << 1 = 0b11100000'00000000

A saturating one would simply do an additional binary OR with the carry (bit shifted out) and all low-order bits.

Essentially, signed arithmetic shift left keeps the sign bit intact, the same exact way an arithmetic shift right does.
The above implements modular arithmetic on both signed and unsigned values, keeping the sign intact.  The saturating one would quarantee the magnitude of the value never decreases.

What use would such an instruction have, besides symmetry?  Several, especially considering how often wraparound to negative values yields unwanted results.  Again, this is arithmetic shift left, not binary.  A single-cycle saturating or modular signed multiplication would achieve the same thing, though, with even more uses.
 

Offline nctnico

  • Super Contributor
  • ***
  • Posts: 30118
  • Country: nl
    • NCT Developments
Reading the thread it becomes clear to me that 'undefined behaviour' in the C standard should read as 'compiler dependant behaviour' which in turn has to be driven by the programmer. Any 3 of the methods (binary shift, arithmetic shift and saturating arithmetic shift) can be correct. It is up to the compiler to implement default behaviour and up to programmer to decide what is preferred if there is a choice.
« Last Edit: October 27, 2022, 03:53:46 pm by nctnico »
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 17773
  • Country: fr
Nah. The std makes a difference between implementation-defined, and undefined behavior. For good reasons.
While it's acceptable (as long as full portability is not a concern) to rely on the former, relying on any specific implementation for UB is just wrong. Don't do it. One good reason is that, if anything is tagged as UB in the std, compilers are free to implement those UBs any way they see fit - including not even caring about what happens - and CHANGE actual behavior at will without ANY notice. UBs are, by definition, non-binding stuff.

Now if you keep insisting and come up with bright arguments such as "but xxx compiler has always treated this one UB in this particular way", this goes in the same basket as the "It compiles, so it worksTM" category. ;D
 
The following users thanked this post: newbrain, DiTBho

Offline magic

  • Super Contributor
  • ***
  • Posts: 8058
  • Country: pl
UB never happens.

If you write
Code: [Select]
if (x)
  do some UB;
else
  do other stuff;
the compiler may not only replace this block with simply
Code: [Select]
do other stuff;but compile other code after and before this block under assumption that x is surely false.

Try to deal with this :box:
 

Offline newbrain

  • Super Contributor
  • ***
  • Posts: 1906
  • Country: se
Nah. The std makes a difference between implementation-defined, and undefined behavior. For good reasons.
While it's acceptable (as long as full portability is not a concern) to rely on the former, relying on any specific implementation for UB is just wrong. Don't do it. One good reason is that, if anything is tagged as UB in the std, compilers are free to implement those UBs any way they see fit - including not even caring about what happens - and CHANGE actual behavior at will without ANY notice. UBs are, by definition, non-binding stuff.

Now if you keep insisting and come up with bright arguments such as "but xxx compiler has always treated this one UB in this particular way", this goes in the same basket as the "It compiles, so it worksTM" category. ;D
Good points.
Not caring: clang does that, at least here:
See this example vs the same with gcc

I would only add that the standard also describes unspecified behaviour.
The major difference with implementation defined behaviour is that for the latter, documentation is mandatory, while not for the former.
In both cases we are considering the behaviour of conformant (if not strictly comformant, if it includes output affecting unspecified or ID behaviour) code.

Definitions from C11:
Quote
3.4
behavior
external appearance or action
3.4.1
implementation-defined behavior
unspecified behavior where each implementation documents how the choice is made
EXAMPLE An example of implementation-defined behavior is the propagation of the high-order bit when a signed integer is shifted right.
3.4.2
locale-specific behavior
behavior that depends on local conventions of nationality, culture, and language that each implementation documents
EXAMPLE An example of locale-specific behavior is whether the islower function returns true for characters other than the 26 lowercase Latin letters.
3.4.3
undefined behavior
behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements
NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).
EXAMPLE An example of undefined behavior is the behavior on integer overflow.
3.4.4
unspecified behavior
use of an unspecified value, or other behavior where this International Standard provides two or more possibilities and imposes no further requirements on which is chosen in any instance
EXAMPLE An example of unspecified behavior is the order in which the arguments to a function are
evaluated
Nandemo wa shiranai wa yo, shitteru koto dake.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Now if you keep insisting and come up with bright arguments such as "but xxx compiler has always treated this one UB in this particular way", this goes in the same basket as the "It compiles, so it worksTM" category. ;D
Do note that my arguments are not that.

I only point out that if basically all C compilers have treated something implementation-defined or undefined behaviour the same way, so that there is a lot of existing C code out there relying on that behaviour, then it is safe to pile on and rely on it, regardless of how someone interprets the standard, because real world always beats theory and the text of the standard.  Sure, a new version of a compiler may decide to do something stupid and break a lot of existing code, but its users won't be happy about the breakage, and will shift to something else.  You can find some of these in POSIX and BSD, especially POSIX threads.

That does not mean that one can just try a small program and see if it works like one thinks it should, and if it does, go ahead and rely on it; NOT AT ALL.

What it means, actually, is that the C standard is a human contract between compiler developers and compiler users, and as such, is not perfect.  Some argue that whenever there is a conflict between the C standard and existing code, the C standard should always be considered the authority.  I disagree, because the standard text is not always unambiguous, and even when it is, it is written by humans and thus not necessarily correct.  In such cases, I argue that the huge mass of existing code and the expectations they embody, is the authority, because only that way is a compiler useful.

It is for this reason I do recommend reading the C standard, but also the sources of widely used C projects, and perhaps the POSIX.1 standard (IEEE Std. 1003.1, freely available on the net), specifically the system interfaces section.  Conflicts do exist between these.  If we start arguing about them in the context of the C standard, we get bogged down into "language lawyerism", which yields no results because trying to convince others that your specific interpretation of the standard text is unlikely to cause others to spend a lot of effort to change their code to conform to your interpretation.
Instead, I say that we accept an imperfect world, and look at the consensus among the kind of C projects we see ourselves in, and conform to that instead.  That way, if we fall, we all fall –– and in that case, we just switch to a "better" C compiler (in reality, one that agrees that our consensus is the useful one).

Better yet, knowing the risks lets us talk to the compiler developers, so that if our favourite compiler(s) are veering towards an unfortunate choice (from our perspective), we can inform them, and ask them to gauge the importance of their specific interpretation of the text of the standards, the benefits of the new interpretation, and the benefits of not breaking a lot of existing code.  It is a human decision, not a technical one.  Even the standards themselves are developed at this level!

I did not invent any of this, of course, I am just describing what kind of an attitude lets a long-term C developer in all complexity levels (from freestanding environments on microcontrollers to GUI application development using Gtk) solve the conflicts in a rational manner without falling into despair.

When there is no conflict, do trust the C standard; I definitely do.
« Last Edit: October 28, 2022, 11:03:18 am by Nominal Animal »
 

Offline peter-hTopic starter

  • Super Contributor
  • ***
  • Posts: 5967
  • Country: gb
  • Doing electronics since the 1960s...
The problem is that so many things can bite you when developing a product.

Unlike probably 99% of people here, I have never worked for anybody since leaving univ in 1978 (Z80 days) so never been "paid to do something". What happens instead is that I get to deal with any s**t a customer discovers in one of my products :) So... I am bloody careful how I do stuff, because the buck doesn't stop anywhere else. This is dramatically different to being a "paid dev" who gets paid for doing the job and then gets paid a second time for fixing his mistakes :) And unless he is absolutely spectacularly crap, or the company went bust as a result of some cockup, he will not get fired.

So I would rank staying with the same compiler, on a given project family, for ever, at around the same level (of risk avoidance) as going to huge trouble to address some undefined behaviour areas. I think I got bitten only once by not knowing C and it was integer promotion (I posted about it; it was the if x == ~y thing where y was a uint8 which upon bit inversion got silently promoted to 0xffffff-something, but once I learnt that (the code never worked) I knew it. I still think it is a stupid feature of C.

However against the above I've had to deal with all kinds of timing dependencies. A 168MHz CPU can easily break min timings on various chips, so code you used, or found on github (some ex 16MHz AVR code, which had only a 50% chance of ever having worked anyway), doesn't work. Or works sometimes. And then nasty stuff like loop replacement by stdlib functions which aren't in your project, if -O2 or -O3 are used (but at least you get a proper crash then).
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline JPortici

  • Super Contributor
  • ***
  • Posts: 3909
  • Country: it
Unlike probably 99% of people here, I have never worked for anybody since leaving univ in 1978 (Z80 days) so never been "paid to do something".
well, i'm paid to do something

Quote
What happens instead is that I get to deal with any s**t a customer discovers in one of my products
and that is one of some things i'm paid for. Trust me that i do not add bugs intentionally, because fixing old stuff is a huge waste of time as my job is developing new stuff, but when there is a problem i HAVE to find a solution, or else. I don't see much of a difference than your situation, other than earning much less money while having much more benefits as i'm an employee. The product either work or it doesn't, there isn't "mostly works", that's an excuse we tell the boss/customer to take some breath off our backs.

Regarding C, as you keep finding out there is too much freedom in the language which means that you have to tell it exactly what to do. I hate implicit casts and conversions, they bite you whenever you're not looking, so i cast variables when necessary, or choose the appropriate type. Also reminds me what i wanted to do in the first place when i look at that part of the code again next year.

Write clean C, be as precise as you can in your intentions, let the compiler figure out how to implement stuff, and don't try to be too clever unless you are absolutely certain you need to. Don't be surprised if your cleverly written C using hoops and tricks perform as well as a simple and clear sequence of statements, optimizers are really good these days.
 
The following users thanked this post: newbrain, MK14


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf