May 14, 2021 Julia
Julia doesn't have a clear function
Main
MATLAB;
clear
If you care about memory usage, you can replace the larger ones with small ones that take up memory. F
or example, if
A
is a large array that you don't need, you can free up memory first with A s
A = 0
The next time you garbage collect, memory is
gc()
Sometimes you define a type but then discover that you need to add a new domain. When you try to do this in rePL, something goes wrong
ERROR: invalid redefinition of constant MyType
Main
in the Main module cannot be redefined.
This can become extremely inconvenient when you are developing new code, and there is a good way to handle it. M
odules can be replaced by redefined methods, so encapsulating all your code in one module can redefine types and constants. Y
ou can't import a type
Main
and redefine it, but you can solve this problem with a module name.
In other words, you can use this workflow when you develop it
include("mynewcode.jl") # this defines a module MyModule
obj1 = MyModule.ObjConstructor(a, b)
obj2 = MyModule.somefunction(obj1)
# Got an error. Change something in "mynewcode.jl"
include("mynewcode.jl") # reload the module
obj1 = MyModule.ObjConstructor(a, b) # old objects are no longer valid, must reconstruct
obj2 = MyModule.somefunction(obj1) # this time it worked!
obj3 = MyModule.someotherfunction(obj2, c)
...
x
argument x to a function and modified its value within the function, but
x
outside the function did not change, why?
Suppose you call a function like this:
julia> x = 10
julia> function change_value!(y) # Create a new function
y = 17
end
julia> change_value!(x)
julia> x # x is unchanged!
10
In Julia, all functions, including
change_value!()
modify the class to which the local variable belongs. I
f
x
defined as an imm changeable object (such as a real number) when called by a function, it cannot be modified; S
imilarly, if
x
defined as a
Dict
you cannot change it to ASCIIString. B
ut the main thing to do
x
Suppose x is an array (or any variable type).
You
x
no longer represent this array, but because the array is a variable object, you can modify the elements of the array:
julia> x = [1,2,3]
3-element Array{Int64,1}:
1
2
3
julia> function change_array!(A) # Create a new function
A[1] = 5
end
julia> change_array!(x)
julia> x
3-element Array{Int64,1}:
5
2
3
Here we define the
change_array!()
which assigns the
5
to the first element of the array.
When we
x
to this function, we notice that
x
still the same array, but the elements of the array have changed.
using
import
No, you can't use
using
in
import
If you want to import a module but only use it in some functions, you have two scenarios:
import
import Foo
function bar(...)
... refer to Foo symbols via Foo.baz ...
end
module Bar
export bar
using Foo
function bar(...)
... refer to Foo.baz as simply baz ....
end
end
using Bar
This means that the type of output can be predicted by the input type. I n particular, this means that the type of output cannot vary depending on the value of the input. The following code is not type stable
function unstable(flag::Bool)
if flag
return 1
else
return 1.0
end
end
This code returns an Int or
Float64
Int
value of the parameter.
Because Julia cannot predict the return value type of a function at compile time, any calculation that uses this function has to consider both possible return types, which makes it difficult to generate fast machine code.
DomainError
?
Some operations make mathematical sense but produce errors:
julia> sqrt(-2.0)
ERROR: DomainError
in sqrt at math.jl:128
julia> 2^-5
ERROR: DomainError
in power_by_squaring at intfuncs.jl:70
in ^ at intfuncs.jl:84
This is caused by type stabilization. F
or
sqrt
most users will use
sqrt(2.0)
a real number instead of a complex number of
1.4142135623730951 + 0.0im
You can
sqrt
as a pluten number when the argument is negative, but this
will no longer
be type stable and
sqrt
will become slow.
In these cases, you can choose the input type to get the output type you want :
julia> sqrt(-2.0+0im)
0.0 + 1.4142135623730951im
julia> 2.0^-5
0.03125
Julia applies integer calculations for machine operations. This means that the range of int values is bounded and is valued between the two bounds, so adding, subtracting, multiplying and multiplying an integer can lead to overflow or downflow, which can lead to some bad consequences, which can be very disturbing at first.
julia> typemax(Int)
9223372036854775807
julia> ans+1
-9223372036854775808
julia> -ans
-9223372036854775808
julia> 2*ans
0
Obviously, this is far from mathematical, and you might think that Julia is less than ideal when it's exposed to users by some advanced programming languages. However, this is a valuable numerical work for efficiency and transparency, and alternatives are even worse.
Here's an option to check for overflows for each integer operation, and to increase the result value to a bigger integer type because of the overflow,
Int128
BigInt
U
nfortunately, this introduces a major burden on each integer operation (think adding a loop counter) - this requires an overflow check when the transmit code executes the program after the arithmetic instruction, and some branches are required to address potential overflow problems. W
orse, this results in every calculation that is unstable when it comes to integers. A
s we mentioned above, the
stability of the type
is the key to effective code generation.
If you can't expect the result of integer operations to be integers, it's impossible to generate fast, simple code done as C and Fortran compilers do.
Another way to avoid changes in the appearance of unstable types is
Int
and
BigInt
into a single mixed integer type, which can be represented by internal changes when the results no longer fit the size of the machine integer. H
owever, this is only a superficial solution to the level of instability in the Julia language, and it only solves a few minor problems by hardening all the same puzzles into the C language so that the hybrid integer type can be successfully implemented. T
his approach can basically work and can even react fairly quickly in many cases, but there are several drawbacks. O
ne problem is that the integer and integer arrays in memory are no longer corresponding to local representing methods in other languages with local machine integers, such as C, Fortran, and so on. T
herefore, to interoperability with these languages, we ultimately need to introduce local integer types anyway. N
o bounded integers have a fixed bit, so they cannot be stored inline in an array with fixed-size slots, and the values of larger integers are always stored in separate heap allocations. O
f course, no matter how subtle the implementation of a mixed integer is, there will always be performance traps or performance declines.
Complex representations, lack of interoperability with the C and Fortran languages, do not represent arrays of integers without additional heap storage, and unpredictable performance characteristics make it difficult to achieve high-performance computing even the most subtle mix of integers.
There is also an option to use a mixed integer or raise it to BigInts, which is implemented using a saturated integer operation that does not change the value even if a number is added to the largest integer value, and again, subtracts the value from the smallest integer value and the value remains the same. That's exactly what matlab ™ can do.
>> int64(9223372036854775807)
ans =
9223372036854775807
>> int64(9223372036854775807) + 1
ans =
9223372036854775807
>> int64(-9223372036854775808)
ans =
-9223372036854775808
>> int64(-9223372036854775808) - 1
ans =
-9223372036854775808
At first glance, this may seem reasonable, since 922337203685477580 is closer to 9223337203685477580 than 922337203685477580, and the integer is still represented in a local way of implementing fixed sizes compatible with the C and Fortran languages. H owever, saturation of integer operations is very problematic. T he first and most obvious problem is that it is not the integer arithmetic of the machine, so each machine performs integer operations to check for underflows or overflows and replaces the results appropriately with typemin (int) or typemax (int) before the instructions required to issue saturation operations can be implemented. T his scales each integer operation from a single, fast instruction to six instructions, possibly including branches. B ut it gets worse - saturated integer arithmetic is not associated. Consider this MATLAB calculation:
>> n = int64(2)^62
4611686018427387904
>> n + (n - 1)
9223372036854775807
>> (n + n) - 1
9223372036854775806
This makes it difficult to write many basic integer algorithms because many common techniques rely on the fact that machine additions and overflows are associated. Consider using the expression (lo and hi) in Julia to find the mid-point of the integer values lo and hi:
julia> n = 2^62
4611686018427387904
julia> (n + 2n) >>> 1
6917529027641081856
Do you see that? N
o problem. T
his is the correct mid-point between 2^62 and
n+2n
should actually be - 461168601842738790.
Now try in MATLAB:
>> (n + 2*n)/2
ans =
4611686018427387904
That's wrong. A dding an a-and-gt; operator to Matlab doesn't help. Saturation occurs because adding n and 2n already destroys the information that is necessary to calculate the correct mid-point.
This is not only because programmers lack the binding nature to rely on such techniques, but also to defeat the optimization integer operations that almost any compiler might want to do. F or example, since Julia's integers use normal machine integer operations, LLVM is free to actively and simply optimize small functions such as f(k) s 5k-1. The machine code for this function is like this:
julia> code_native(f,(Int,))
.section __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 1
push RBP
mov RBP, RSP
Source line: 1
lea RAX, QWORD PTR [RDI + 4*RDI - 1]
pop RBP
ret
The actual body of the function is a single
lea
instruction that is multiplied and multiplied immediately when the integer is calculated.
When f is embedded in another function, it is more advantageous:
julia> function g(k,n)
for i = 1:n
k = f(k)
end
return k
end
g (generic function with 2 methods)
julia> code_native(g,(Int,Int))
.section __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 3
push RBP
mov RBP, RSP
test RSI, RSI
jle 22
mov EAX, 1
Source line: 3
lea RDI, QWORD PTR [RDI + 4*RDI - 1]
inc RAX
cmp RAX, RSI
Source line: 2
jle -17
Source line: 5
mov RAX, RDI
pop RBP
ret
Since
f
call is inline, the end of the loop body is just a single
lea
instruction.
Next, if we fix the number of iterations in the loop, we can consider what happened:
julia> function g(k)
for i = 1:10
k = f(k)
end
return k
end
g (generic function with 2 methods)
julia> code_native(g,(Int,))
.section __TEXT,__text,regular,pure_instructions
Filename: none
Source line: 3
push RBP
mov RBP, RSP
Source line: 3
imul RAX, RDI, 9765625
add RAX, -2441406
Source line: 5
pop RBP
ret
Because the compiler knows the connection between addition and multiplication of integers and multiplication is assigned a higher priority than division - both are true saturation operations - they can optimize the entire loop so that only multiplication and addition remain. The saturation algorithm completely defeats this optimization because binding and distribution can fail at each iteration in the loop, and the different consequences depend on which iteration fails.
The saturation integer algorithm is just one example of a really poor choice of language semantics, which prevents all effective performance optimization. T here are a lot of things that are difficult in C-language programming, but integer overflow is not one of them, especially in 64-bit systems. F or example, if I use an integer that might become bigger than 2-63-1, I can easily predict. D o you want to ask yourself if I'm traversing the actual things stored on your computer? T hen I can confirm that the numbers don't get that big. T hat's a guarantee, because I don't have that much storage space. A m I really counting the real thing? U nless they are sand or atomic particles in the universe, 2^63-1 is large enough. A m I calculating factority? T hen you can confirm that they may be particularly large - I should use BigInt. D o you understand? It's easy to tell.
A type can be declared without specifying the type of field:
julia> type MyAmbiguousType
a
end
This
a
to be of any type. T
his is often useful, but it has one drawback: for
MyAmbiguousType
the compiler will not be able to generate efficient code. T
he reason is that the compiler uses the type of the object rather than the value to decide how to build the code.
Unfortunately,
MyAmbiguousType
can only infer very little information:
julia> b = MyAmbiguousType("Hello")
MyAmbiguousType("Hello")
julia> c = MyAmbiguousType(17)
MyAmbiguousType(17)
julia> typeof(b)
MyAmbiguousType (constructor with 1 method)
julia> typeof(c)
MyAmbiguousType (constructor with 1 method)
b
and
c
the same types, but the underlying represents of their data in memory are very different. E
ven if you only store values in the domain of
a
the fact that the memory representations of
Uint8
and
Float64
are different means that the CPU needs to handle them with two different instructions. B
ecause the required information in the type is not available, such a decision has to be made at runtime. T
his slows down performance.
You can do better
a
type of a. H
ere, we note that
a
may be any of several types, in which case the natural solution is to use parameters.
For example:
julia> type MyType{T<:FloatingPoint}
a::T
end
This is a better choice than the following code
julia> type MyStillAmbiguousType
a::FloatingPoint
end
Because the first version specifies the type of wrapper object. For example:
julia> m = MyType(3.2)
MyType{Float64}(3.2)
julia> t = MyStillAmbiguousType(3.2)
MyStillAmbiguousType(3.2)
julia> typeof(m)
MyType{Float64} (constructor with 1 method)
julia> typeof(t)
MyStillAmbiguousType (constructor with 2 methods)
a
type of a's domain can be easily
m
but not by the type of
t
In fact, in
t
is the type of domain that can change
a
julia> typeof(t.a)
Float64
julia> t.a = 4.5f0
4.5f0
julia> typeof(t.a)
Float32
Conversely,
m
is constructed,
m.a
cannot be changed:
julia> m.a = 4.5f0
4.5
julia> typeof(m.a)
Float64
a
that the type of a can be known from the type of m and
m.a
cannot be
m
t
instead of a class like
m
Of course, all this is true only when
m
with a specific type.
We can break one point by explicitly constructing it with abstract classes:
julia> m = MyType{FloatingPoint}(3.2)
MyType{FloatingPoint}(3.2)
julia> typeof(m.a)
Float64
julia> m.a = 4.5f0
4.5f0
julia> typeof(m.a)
Float32
For all practical purposes, these objects behave
MyStillAmbiguousType
It makes sense to compare all the code generated by a simple program:
func(m::MyType) = m.a+1
Use:
code_llvm(func,(MyType{Float64},))
code_llvm(func,(MyType{FloatingPoint},))
code_llvm(func,(MyType,))
The results are not shown here because of the length, but you might as well try it yourself. B ecause in the first case, the type is fully specified, the compiler does not need to generate any code at runtime to resolve the type's problems. This results in shorter code encoding faster.
The same examples as the best ones applied in the previous section also apply in container types:
julia> type MySimpleContainer{A<:AbstractVector}
a::A
end
julia> type MyAmbiguousContainer{T}
a::AbstractVector{T}
end
For example:
julia> c = MySimpleContainer(1:3);
julia> typeof(c)
MySimpleContainer{UnitRange{Int64}} (constructor with 1 method)
julia> c = MySimpleContainer([1:3]);
julia> typeof(c)
MySimpleContainer{Array{Int64,1}} (constructor with 1 method)
julia> b = MyAmbiguousContainer(1:3);
julia> typeof(b)
MyAmbiguousContainer{Int64} (constructor with 1 method)
julia> b = MyAmbiguousContainer([1:3]);
julia> typeof(b)
MyAmbiguousContainer{Int64} (constructor with 1 method)
For
MySimpleContainer
are fully specified by their type and parameters, so the compiler can generate optimized functionality.
In most cases, this may be enough.
Although the compiler can now do its job perfectly, there are times when you might want your
a
on the type
of
element of a.
In general, the best way to do this is to wrap your specific actions (in this place
foo
in a separate function:
function sumfoo(c::MySimpleContainer)
s = 0
for x in c.a
s += foo(x)
end
s
end
foo(x::Integer) = x
foo(x::FloatingPoint) = round(x)
This allows the compiler to generate optimized code in all cases while keeping it simple.
However, sometimes you need to declare different versions of external functions based on the different element types of
a
You can do it like this:
function myfun{T<:FloatingPoint}(c::MySimpleContainer{Vector{T}})
...
end
function myfun{T<:Integer}(c::MySimpleContainer{Vector{T}})
...
end
Vector{T}
but we'll also want
UnitRange{T}
or other abstract classes.
To prevent this tedious situation, you can use two variables in
MyContainer
declaration:
type MyContainer{T, A<:AbstractVector}
a::A
end
MyContainer(v::AbstractVector) = MyContainer{eltype(v), typeof(v)}(v)
julia> b = MyContainer(1.3:5);
julia> typeof(b)
MyContainer{Float64,UnitRange{Float64}}
Notice a somewhat surprising fact that T is
T
a
and we'll be back to that in a minute.
In this way, one can write functions like this:
function myfunc{T<:Integer, A<:AbstractArray}(c::MyContainer{T,A})
return c.a[1]+1
end
# Note: because we can only define MyContainer for
# A<:AbstractArray, and any unspecified parameters are arbitrary,
# the previous could have been written more succinctly as
# function myfunc{T<:Integer}(c::MyContainer{T})
function myfunc{T<:FloatingPoint}(c::MyContainer{T})
return c.a[1]+2
end
function myfunc{T<:Integer}(c::MyContainer{T,Vector{T}})
return c.a[1]+3
end
julia> myfunc(MyContainer(1:3))
2
julia> myfunc(MyContainer(1.0:3))
3.0
julia> myfunc(MyContainer([1:3]))
4
As you can see, this approach allows you to focus on both element type
T
and array
A
However, there is one remaining problem: we did
A
to include element type
T
so it is entirely possible to construct such an object:
julia> b = MyContainer{Int64, UnitRange{Float64}}(1.3:5);
julia> typeof(b)
MyContainer{Int64,UnitRange{Float64}}
To prevent this, we can add an internal constructor:
type MyBetterContainer{T<:Real, A<:AbstractVector}
a::A
MyBetterContainer(v::AbstractVector{T}) = new(v)
end
MyBetterContainer(v::AbstractVector) = MyBetterContainer{eltype(v),typeof(v)}(v)
julia> b = MyBetterContainer(1.3:5);
julia> typeof(b)
MyBetterContainer{Float64,UnitRange{Float64}}
julia> b = MyBetterContainer{Int64, UnitRange{Float64}}(1.3:5);
ERROR: no method MyBetterContainer(UnitRange{Float64},)
The internal constructor
A
element type of
T
Unlike in many other languages, such as C and Java, there is no "null" value in Julia. W
hen a reference (a variable, a domain of an object, or an array element) is unitialized, accessing it immediately throws an error.
This can be
isdefined
function.
Some functions are used only for their side effects and do not require a return value. I
n this case, the convention
nothing
which is just a
Nothing
object. T
his is a normal type without a domain; S
ome language structures that cannot be valued are also unified as
nothing
such as
if false; end
。
Note
Nothing
(capital) is the type of
nothing
and should only be used in a type-by-demand environment (for example, a declaration).
You may occasionally see
None
which is completely different. I
t is an empty, or "bottom" type, a type that has no value and no subtypes (subtypes, except for itself).
You generally do not need to use this type.
An empty group
()
is another type of none.
But it shouldn't really be considered nothing but a zero-value group.
If you want a stable code base, you might prefer Julia's release. Typically released every 6 months, gives you a stable writing platform.
If you don't mind falling slightly behind the latest error fixes and changes, but find more attractive changes a little faster, you might prefer the Julia beta version. In addition, these binary files are tested before they are published to ensure that they are fully functional.
If you want to take advantage of the latest updates in your language, you might prefer to use Julia's nightly version and don't mind that it doesn't work occasionally.
Finally, you can also consider building Julia for yourself at the source. T his option is primarily for individuals who are comfortable with the command line or interested in learning. If this describes you, you may also be interested in reading our guidelines.
Links to these download types can be found on the http://julialang.org/downloads/ page. Note that not all versions of Julia are available on all platforms.
Outdated features are removed after subsequent releases. For example, features marked as obsolete in a 0.1 release will not be used in a 0.2 release.
First you should build the
make debug
make debug.
Below, the
(gdb)
means that you need to enter it under gdb prompt.
The main challenge is that both Julia and gdb need to have their own terminals to allow you to interact with them. O
ne approach is to use gdb's
attach
feature to debug a running Julia session. H
owever, in many systems, you need to use root access to make this work.
Here's a way to implement it using only user-level permissions.
The first time you do this, you need to define a script, here called
oterm
that contains the following lines:
ps
sleep 600000
Let it
chmod +x oterm
Right now:
xterm -e oterm &
You will see a new window pop up, which will be called Terminal 2.
gdb julia-debug
.
You will
julia/usr/bin
(gdb) tty /dev/pts/#
is the number that is displayed after
pts/
terminal 2.
#
(gdb) run
(gdb) b codegen.cpp:2244
(gdb) c
Julia's execution
M-x gdb
then go
julia-debug
(this can be easiest to find from julia/usr/bin, or you can specify the full path)
(gdb) run
(gdb)
prompt
(gdb) b codegen.cpp:2244
(gdb) c
c