Julia's loop scope behavior differs from Fortran?

Exactly. Put simply, no edit to the manual will help if people don’t know how to look up sections, just like with real books. You can put flashing neon lights on the section and it wouldn’t help if the reader hasn’t reached the page.
That’s not to say this can’t be improved ever. I just remembered this topic where it turned out typing == and === in the search bar didn’t return the right sections in Base or Manual.

I think a big issue is that the manual is not at all easy to search. Something simple that might help is to make it easier for people to restrict their searches – for example, when I want to find something in the manual, half the time it’s buried under hundreds of function or type definitions from the base and standard library.

Something as simple as letting me “just” restrict my search to the manual contents would be helpful. (and if that functionality exists, I haven’t been able to find it. Usually, I just rely on Google searches, which invariably take me to a version of the manual years out of date. Then I have to use that as a baseline for lookin in the current manual, and hope that the particular section hasn’t been moved or reorganized.)

The manual has a lot of great content, but (IMHO) it’s too hard to find any of it unless you already know it inside and out.

3 Likes

EDIT: I get the exact same performance between them (and -march=native doesn’t seem to matter).
Compiling with

gfortran -O3 -march=native rootloop.f90 -shared -fPIC -o libloop.so
gfortran -Ofast -march=native rootloop.f90 -shared -fPIC -o libloopfast.so

Yields:

julia> using BenchmarkTools

julia> function loop(x,n)
         s = 0.
         for i in 1:n
           r = sqrt(i)
           s = s + x*r
         end
         s
       end
loop (generic function with 1 method)

julia> function loopfast(x,n)
         s = 0.
         @fastmath for i in 1:n
           r = sqrt(i)
           s = s + x*r
         end
         s
       end
loopfast (generic function with 1 method)

julia> floop(x,n) = ccall((:loop_,"libloop.so"),Float64,(Ref{Float64},Ref{Int64}),x,n)
floop (generic function with 1 method)

julia> floopfast(x,n) = ccall((:loop_,"libloopfast.so"),Float64,(Ref{Float64},Ref{Int64}),x,n)
floopfast (generic function with 1 method)

julia> x = rand() ; n = 10_000_000;

julia> @btime loop($x, $n)
  13.054 ms (0 allocations: 0 bytes)
7.112252395049944e9

julia> @btime floop($x, $n)
  13.054 ms (0 allocations: 0 bytes)
7.112252395049944e9

julia> @btime loopfast($x, $n)
  6.982 ms (0 allocations: 0 bytes)
7.112252395049599e9

julia> @btime floopfast($x, $n)
  6.982 ms (0 allocations: 0 bytes)
7.1122523950496235e9
1 Like

Thanks for the tip, yet now I am trying to reproduce exactly what I did before and I do not see that difference in performance anymore… :neutral_face: … Now I see that -O3 is enough to pair up performances, maybe I forgot to add that when I compiled the code for the first time. How unfair with old good Fortran I have been :slight_smile: .

1 Like

Ha, I admit I didn’t try without -march=native to confirm. I just did and got the exact same performance as with it when using -O3. When using -Ofast, it was faster without -march=native. lol

The problem is that sqrt limits the performance of this function, and that SIMD square roots with 4 or 8 elements aren’t any faster per element than SIMD square roots with 2 elements in terms of number of clock cycles.
That is, on Skylake/Cascadelake-X, sqrt on 1 or 2 Float64 are equally fast, but on 4 takes twice as long, and 8 twice as long again. Note that this is architecture specific. Agner Fog’s instruction tables suggest that sqrtpd on Zen2 CPUs is equally fast, independent of vector sizes, meaning if using -Ofast -march=native should be faster on them than just -Ofast. Similarly, starting Julia normally and using @fastmath should be faster than starting Julia with julia -Cgeneric.

julia> @btime floopfastgeneric($x, $n)
  6.527 ms (0 allocations: 0 bytes)
9.735648448452713e9

julia> @btime floopfast($x, $n)
  6.982 ms (0 allocations: 0 bytes)
9.735648448452318e9

julia> 6.982 * 4.3/4.6
6.526652173913044

My CPU runs AVX(2) and light AVX512 code at 4.3 GHz, and SSE code at 4.6 GHz.
The ratio 4.3/4.6 equals the ratio in relative times.

So, equivalent speed in terms of number of clock cycles needed, but faster clock speed when not using -march=native gives the generic version the edge.

2 Likes

Interesting! So… I must ask: Why do we have scope below function-level, then?

The way it looks like to me is: If we removed scoping of let/for/while within functions, then some code that currently throws UndefVarError would instead do something else. And that’s it – code that doesn’t throw UndefVarError would not change behavior. Or am I missing something?

5 Likes

I don’t know why but a lot of people including me were asking about scope rules in the last few days, maybe astrology is real.

3 Likes

welcome to my life

10 Likes

Thanks lungben, the local DataVar statement seems to have gotten this kind of loop to work as I expected it to work in the first place.

While the discussion that my question triggered is all very interesting, and people seem to be repeating positions from previous debates, as a relative newbie to Julia, I’d like to put my two cents in on this.

First, while the documentation has some good info, it is incredibly sparse. I spend 2 or 3 times more time clicking back and forth between documentation pages and googling key phrases, trying to find specific useful info, than I spend actually reading documentation. There is an extreme shortage of actual programming examples, demonstrating the proper syntax of how to use the commands, and often I can’t even find a listing or explanation of the options available for the commands. There are also very few books on Julia, since it is a relatively new language, so it is very hard to pick up from scratch. Without this discussion board, I would have completely given up on the Julia programming language within a week.

Second, the entire “scope” setup of variables in Julia is a horror, plain and simple. I don’t care how many advantages it creates down the road, just writing a simple program with a detailed control flow leads constantly to errors and spurious results. Rather than debating how much detail is optimal to fill in on the “scope of variables” documentation, maybe the problem is that the entire approach is baffling from a common sense point of view. (Even if statement blocks sometimes create new scopes!?) Julia provides so many advantages in mathematical power, why completely reinvent the wheel on basic programming strategy? I just lost a week of productivity trying to figure out where my variables were going, it is incredibly frustrating.

Well, I do not think that the guys working with this for 10 years and developing this great language should spend their time answering this (isn’t that wonderful that many of them answered your questions?), but I can answer from a personal perspective being someone coming from Fortran and relatively recently migrating to Julia:

Yes, there are some quirks when changing from Fortran to Julia. We feel at the beginning quite unsafe using these dynamically typed language. I missed the possibility of declaring every variable, being sure that if I mistyped a letter somewhere the compilation returns an error, etc. Some bugs are harder to find in Julia than in Fortran.

That said, from my perspective the facility with which one can write fast code, use packages developed by others, parallelize stuff even in GPUs, produce high-level results (plots, animations, interfaces), greatly justifies me migrating to Julia from Fortran. Also in the process I am learning a lot of things about computer programming that I was not aware of. I am becoming a better programmer, I think, in the process. Testing things is much easier, experimenting small functions and alternatives is much faster. The final codes are much cleaner and of general use, and you can deploy them as packages that people can use without having to compile the whole thing. That is just great.

And Julia is a language which allows all that without forcing me to stop doing what I think I was good at, which was writing efficient code in Fortran by using only the most basic stuff of every language (loops and conditionals). Other dynamically typed languages never convinced me because they forced me to rethink how to formulate every problem to get reasonable performance.

It is up to you to see if the effort is worthwhile. But those scoping things are not as bad as they look like. One readily gets used to that and life goes on.

And, effectively, I also probably thought that I could have done something better, but as time goes by I see that, first, I have no idea of the compromises that the language has to do to offer all that it offers and, second, I have no idea how to create a new language. So at most I may contribute from a user perspective saying that something is hard to understand and could be documented somehow differently.

13 Likes

I’m extremely grateful for everyone developing this language, and answering my questions on the discussion board. I’m not at all saying that I could do a better job at designing a programming language (or even design one at all).

I’m just trying to put in my vote that too much freedom in typing or defining variables provides too much “scope” for error, so to speak. Maybe I’m old school, but a more rigid approach to variables that doesn’t let you make errors is more my style. There seems to be a big debate about this, and early versions of Julia were different, so I’m just expressing a preference for that.

40 posts were split to a new topic: Julia’s assignment behavior differs from Fortran?

If statements never introduce new scopes.

6 Likes

Given your perspective, I would suggest declaring all your local variables at the top of each function body like you are required to in Fortran and using for outer loops instead of for loops. Then Julia will work pretty much like Fortran.

Note that Fortran is a language without closures — and very much a pre-functional programming language in general. Letting variables have expansive scopes is a perfectly fine design in the absence of closures but a constant source of footguns in languages that have them. Python followed Fortran’s lead in this but then later added closures, resulting to some very unfortunate interactions. This is explained in detail in the post that I linked to above.

3 Likes

I would not suggest that, because it is unnatural in Julia and does not result in the same behavior in general as in Fortran, as I posted above. It leads to further confusion. We just need to adapt to the new language.

Curiously, I was first presented to Julia with that perspective, that it could just be “Fortran”-like. It does not. We Fortran programmers just have to get over it.

1 Like

It does in terms of scope. I’m not suggesting adding types to the variables because that does not work like it does in Fortran, but if you want every local to live for the duration of each function body, just declare all of them at the top of the function.

2 Likes

Sure, that is clear. I am only saying that the expectations of a Fortran programmer will not be satisfied with that. We fall down into other problems according to the expectations that we had from our Fortran code.

Definitely. Learning a new language involves learning some new things. The Julia scope rules are very simple:

  • loops, comprehensions, and let & try blocks have local scope
  • if x is already a local then x = assigns it
  • if x isn’t already a local then x = creates it

That’s all. There’s some relaxation of this in the REPL to make things more convenient but you generally don’t have to think about that (it’s very “do what I mean” by design). And if you do something in the REPL and move it to a file such that it works differently, you will get a clear warning telling you what to fix.

The only objection in this thread seems to be distaste that there are blocks other than functions that have their own scope. Which seems very specific to Fortran — even in C loops have their own scope. The trend in language design is very clearly towards more and smaller scopes — precisely because of the bad interaction of large scopes with functional programming (and concurrency, for that matter). But if someone really hates that loops have scope, they can just declare locals at the top of each function.

13 Likes

It would be great to condense down these lessons and add a Fortran section to https://docs.julialang.org/en/v1/manual/noteworthy-differences/

5 Likes