C# fundamentally not portable?
Asked Answered
F

5

17

I've been using C# for a while, and have recently started working on adding parallelism to a side project of mine. So, according to Microsoft, reads and writes to ints and even floats are atomic

I'm sure these atomicity requirements workout just fine on x86 architectures. However, on architectures such as ARM (which may not have hardware floating point support), it seems these guarantees will be hard.

The problem is only made more significant by the fact that an 'int' is always 32-bits. There are many embedded devices that can't atomically perform a 32-bit write.

It seems this is a fundamental mistake in C#. Guaranteeing the atomicity of these data types can't be done portably.

How are these atomicity guarantees intended to be implemented on architectures where there are no FPUs or 32-bit writes?

Fifield answered 29/9, 2010 at 19:4 Comment(14)
Is this a question?Radioman
Lots of smart people have been involved in writing and implementing this spec, I am sure they have thought of this before issuing the guarantee. Perhaps Eric Lippert can elucidate?Biamonte
@SLaks: Yes: How are these atomicity guarantees intended to be implemented on architectures where there are no FPUs or 32-bit writes?Fifield
@Fifield - please edit the question and add the actual question to it instead of as a comment.Biamonte
@Oded: I really hope so. One of the reasons I enjoy C# is how easy it is to write portable code.Fifield
I believe that's what's referred to as an "implementation detail" ... I'm not aware of any ARM-based CLIs ... am I missing something?Kenlee
i wasn't aware of this - does that mean if i write, say, an entity class, with only those types, it's thread safe by virtue of the spec? in c++ I've used locking on booleans because my fear of it being compiled on a machine that uses >1 instruction to modify memory...Gobang
The atomicity of int/ref types is not a C# feature but part of the CLR (which is an implementation of a CLI). The question might be answered if someone can find out how the Compact and Micro frameworks deal with this.Extracurricular
not to be a smarty but i personally didn't have any trouble tacking on the implied question of, "am I right?" to the post :)Gobang
@Henk Holterman. It's section 5.5 of the c# spec. Doesn't this mean that it is a feature regardless of where its implemented (Compiler, CLR, Hardware etc).Begley
@Kenlee - Doesn't Windows Phone 7 use C#? I'm pretty sure WP7 phones will be ARM based.Fifield
@Fifield - If so then I imagine Microsoft has written a CLI for Snapdragon processors. A CLI implementation is specific to the hardware it runs on. en.wikipedia.org/wiki/Common_Language_InfrastructureKenlee
@Kenlee - "I'm not aware of any ARM-based CLIs" - .NET Compact Framework (including support for ARM processor) has been around since framework 1.0, VS.NET (2002), and Pocket PC / Windows MobileCapparidaceous
@Capparidaceous - Interesting, I haven't had the opportunity to develop against CF; that's good to know.Kenlee
V
7

There are two issues with regard to "portability":

  1. Can an practical implementation of a language be produced for various platforms
  2. Will a program written in a language be expected to run correctly on various platforms without modification

The stronger the guarantees made by a language, the harder it will be to port it to various platforms (some guarantees may make it impossible or impractical to implement the language on some platforms) but the more likely it is that programs written in the language will work without modification on any platform for which support exists.

For example, a lot of networking code relies upon the fact that (on most platforms) an unsigned char is eight bits, and a 32-bit integer is represented by four unsigned chars in ascending or descending sequence. I've used a platform where char was 16 bits, sizeof(int)==1, and sizeof(long)==2. The compiler author could have made the compiler simply use the bottom 8 bits of each address, or could have added a lot of extra code so that writing a 'char' pointer would shift the address right one bit (saving the lsb), read the address, update the high or low half based upon the saved address lsb, and writing it back. Either of those approaches would have allowed the networking code to run without modification, but would have greatly impeded the compiler's usefulness for other purposes.

Some of the guarantees in the CLR mean that it is impractical to implement it in any platform with an atomic operation size smaller than 32 bits. So what? If a microcontroller needs more than a few dozen Kbytes of code space and RAM, the cost differential between 8-bit and 32-bit is pretty small. Since nobody's going to be running any variation of the CLR on a part with 32K of code space and 4K of RAM, who cares whether such a chip could satisfy its guarantees.

BTW, I do think it would be useful to have different levels of features defined in a C spec; a lot of processors, for example, do have 8-bit chars which can be assembled into longer words using unions, and there is a lot of practical code which exploits this. It would be good to define standards for compilers which work with such things. I would also like to see more standards at the low end of the system, making some language enhancements available for 8-bit processors. For example, it would be useful to define overloads for a function which can take a run-time-computed 16-bit integer, an 8-bit variable, or an inline-expanded version with a constant. For often-used functions, there can be a big difference in efficiency among those.

Vidal answered 29/9, 2010 at 22:48 Comment(0)
S
12

It's not too difficult to guarantee the atomicity with runtime checks. Sure, you won't be as performant as you might be if your platform supported atomic reads and writes, but that's a platform tradeoff.

Bottom line: C# (the core language, not counting some platform-specific APIs) is just as portable as Java.

Solomon answered 29/9, 2010 at 19:17 Comment(10)
Can you explain what you mean by: "It's not too difficult to guarantee the atomicity with runtime checks"?Fifield
If the only way one ever wrote to a 32-bit data type were via library routine, that routine could make sure it never started a 32-bit write while another was in progress. I suspect that's what will happen with things like Interlocked.Increment on 64-bit values, since there is an explicit warning that 64-bit Interlocked operations are only guaranteed atomic with regard to other 64-bit Interlocked operations.Vidal
@Jake: it's called a semaphore.Solomon
Easy - generate native code that surrounds all accesses to those types with appropriate locking. It'll probably be slow, but the guarantees can be met.Jerusalem
@Andrew - I suppose that makes sense. You could lock around every single read / write of an int. That seems like it'd cause a huge overhead though. I guess that'd make it portable though. But then I'd have to say: C# is fundamently inefficient on certain architectures. And, it could be made efficient by removing this requirement that these types behave atomically.Fifield
@Jake: The whole point of higher-level languages like C#, Java, and Python are that we are willing to trade off "efficiency" for "rapid development", "portability" (to an extent), and "maintainability" (although it's quite possible to write unmaintainable code in any language). Is C# faster than raw C? Probably not. Is raw C faster than platform assembly? Probably not. Does it matter? Not really.Solomon
@Solomon - I like C#. I want C# to be very portable. It seems they could have dropped the 'ints / floats' are atomic requirement and made it significantly moreso. What's wrong with using Interlocked* methods when atomicity is needed?Fifield
@Jake: and what's wrong with not using them when it's guaranteed by the framework? IMO, the guarantee solves far more problems than dropping it would.Solomon
@Randolpho: How does on go about not using the feature that reads and writes to an int are atomic?Fifield
@Jake: my point is that we shouldn't be forced to use Interlocked* methods when the framework can take care of that for us -- that's the whole point of a framework like .NET or Java. You might as well be arguing that we should go back to unmanaged memory. We as developers know the pros and cons of managed memory and choose the pros because they outweigh the cons.Solomon
Q
8

The future happened yesterday, C# is in fact ported to a large number of embedded cores. The .NET Micro Framework is the typical deployment scenario. Model numbers I see listed as native targets are AT91, BF537, CortexM3, LPC22XX, LPC24XX, MC9328, PXA271 and SH2.

I don't know the exact implementation details of their core instruction set but I'm fairly sure that these are all 32-bit cores and several of them are ARM cores. Writing threaded code for them requires a minimum guarantee and atomic updates for properly aligned words is one of them. Given the supported list and that 4 byte atomic updates for aligned words is trivial to implement in 32-bit hardware, I trust they all do in fact support it.

Quilting answered 29/9, 2010 at 19:25 Comment(6)
I've looked up each of those architectures. Not only are they all 32-bit, they also have dedicated FPU's. This still doesn't answer the question of architectures that are neither of those.Fifield
@Jake: I wouldn't hold my breath waiting for C# to be ported to them. There's little point, executing IL does require a bit of horse power.Quilting
Personally, I'm still holding out for a CLI implementation that can run on my old NES.Vins
@Dan: your old NES getting ported to CLI is however likely :)Quilting
@Dan: lol, who woulda thought all those old NES games would be so portable?Fifield
@Jake, I don't think the NES emulator writers would agree. The early cartridge-based console games tended to operate at very low levels, often relying on odd quirks in the behavior of the NES CPU and chipsets. Because they were cartridges, they could also include their own custom electronics, which sometimes included custom co-processors for special effects not supported by the main CPU. Back in that era, portability was not really a concern; developers were relying on the minute peculiarities of each platform, trying to push the limits of performance.Vins
V
7

There are two issues with regard to "portability":

  1. Can an practical implementation of a language be produced for various platforms
  2. Will a program written in a language be expected to run correctly on various platforms without modification

The stronger the guarantees made by a language, the harder it will be to port it to various platforms (some guarantees may make it impossible or impractical to implement the language on some platforms) but the more likely it is that programs written in the language will work without modification on any platform for which support exists.

For example, a lot of networking code relies upon the fact that (on most platforms) an unsigned char is eight bits, and a 32-bit integer is represented by four unsigned chars in ascending or descending sequence. I've used a platform where char was 16 bits, sizeof(int)==1, and sizeof(long)==2. The compiler author could have made the compiler simply use the bottom 8 bits of each address, or could have added a lot of extra code so that writing a 'char' pointer would shift the address right one bit (saving the lsb), read the address, update the high or low half based upon the saved address lsb, and writing it back. Either of those approaches would have allowed the networking code to run without modification, but would have greatly impeded the compiler's usefulness for other purposes.

Some of the guarantees in the CLR mean that it is impractical to implement it in any platform with an atomic operation size smaller than 32 bits. So what? If a microcontroller needs more than a few dozen Kbytes of code space and RAM, the cost differential between 8-bit and 32-bit is pretty small. Since nobody's going to be running any variation of the CLR on a part with 32K of code space and 4K of RAM, who cares whether such a chip could satisfy its guarantees.

BTW, I do think it would be useful to have different levels of features defined in a C spec; a lot of processors, for example, do have 8-bit chars which can be assembled into longer words using unions, and there is a lot of practical code which exploits this. It would be good to define standards for compilers which work with such things. I would also like to see more standards at the low end of the system, making some language enhancements available for 8-bit processors. For example, it would be useful to define overloads for a function which can take a run-time-computed 16-bit integer, an 8-bit variable, or an inline-expanded version with a constant. For often-used functions, there can be a big difference in efficiency among those.

Vidal answered 29/9, 2010 at 22:48 Comment(0)
H
6

That's what the CLI is for. I doubt they will certify an implementation if it isn't compliant. So basically, C# is portable to any platform that has one.

Haslett answered 29/9, 2010 at 19:16 Comment(9)
That's disappointing, as this requirement means the CLI isn't very portable.Fifield
@Fifield Portable CLI's are not a reasonable expectation. JVM's and HALs aren't portable. The whole point of these technologies is that they enable the other bits to be portable.Begley
@Jake: if you really think C is as portable as you seem to wish C# was, you don't know nearly as much about C as you think you do.Solomon
@Solomon - What part of C isn't portable? I'm well versed with the standard, and am genuinely interested.Fifield
But then again C doesn't promise that much right? It might have changed with C99 but IIRC C/C++ promises for instance: 1==sizeof(char)<=sizeof(short)<=sizeof(int)<=sizeof(long). So a C compiler where sizeof(char) == sizeof (int) would be standard conforming according to that definition. In addition IIRC char can be 7 or 9 bits as long as sizeof(char) == 1. I bet alot of programs out there aren't able to cope with a 7bit char. Once again my knowledge might be out of sync since I have not looked into the details of C99.Curie
@Jake: @FuleSnabel has provided an excellent example of my point. C is portable -- to a point. You can write very portable C -- usually. But there are frequently little implementation details that crop up.Solomon
@FuleSnabel: C99 guarantees that a char is at least 8 bits.Fifield
@Randolpho: It's not a good point. Flexible type sizes are why C is so portable. My original question is an example of the C# standard being unreasonably strict. As a result, some architectures will never be able to support C#.Fifield
@Jake: I think you may have a different definition of "portable" than I do. If I have to jump through hoops and deal with edge cases to write code that runs the same on two different platforms, the language is not portable.Solomon
V
3

Excessively weakening guarantees for the sake of portability defeats the purpose of portability. The stronger the guarantees, the more valuable the portability. The goal is to find the right balance between what the likely target platforms can efficiently support with the guarantees that will be the most useful for development.

Vins answered 29/9, 2010 at 21:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.