I'm comparing Python, D, Nim and Go. Here's one small Nim test program (it works):
import tables
var
map = initTable[int, int]()
for i in 0..<10_000_000:
map[i] = i
for i in 0..<3:
for j in 0..<10_000_000:
map[j] = map[j] + 1
Runs really fast, 1.2 seconds using 805MB (Python is 8.76s 1098MB, D is 7.17s 881MB, Go is 4.0s 627MB). Since I don't need 64-bit integers in the table, I tried this to get the space down:
import tables
var
map = initTable[int32, int32]()
for i in 0..<10_000_000:
map[i] = i
for i in 0..<3:
for j in 0..<10_000_000:
map[j] = map[j] + 1
p3int32fail.nim(7, 6) Error: type mismatch: got <Table[system.int32, system.int32], int, int>
but expected one of: (list of type matches)
I'm guessing this is because the subrange type is int..int. It works if I add i32 to the 10M constants. It also works if i and j are 32-bit but the map is 64-bit like the original, so Nim will automatically promote from int32 to int without a type error. However, also confusingly, it gives a type error if I add u32 to the 10M constants.
My suggestion is that instead of always having int..int types, subranges have the smallest type that will contain the subrange. So a subrange of 0..10 would be of type uint8. To make that work, Nim would also have to promote uintx to int the same way it promotes intx to int. I can't see a reason why it will promote int32 to int but not uint32 to int.
I'm just getting started with Nim so may be totally confused about how things work.
Test results: using an int32 map reduces Nim memory usage to 537M (349M for Go, no change for D). Specifying the initial table size as 10000000 brings Nim memory usage down to 269M (188 for Go). But Go takes 3 seconds while Nim is only 0.89s.
My suggestion is that instead of always having int..int types, subranges have the smallest type that will contain the subrange. So a subrange of 0..10 would be of type uint8. To make that work, Nim would also have to promote uintx to int the same way it promotes intx to int. I can't see a reason why it will promote int32 to int but not uint32 to int.
Because uint32 might not fit into an int on 32 bit machines where int is defined to take 32 bits.
If I change the table to [int64, int64], Nim still gives type errors when the constants are changed to u32, even though it can promote uint32 to int64. i32 constants still work.
Also, I don't understand your reasoning about uint32 not fitting into an int on 32-bit machines. If I am compiling on a machine where int is 64 bits (I am), then doesn't int mean exactly the same thing as int64? I guess what I'm getting at is, does a program that compiles+runs on a 64-bit machine where int is 64-bits also have to compile+run correctly on a 32-bit machine, where int is 32-bits? It doesn't seem possible unless int is restricted to 32 bits in both cases.
Here is a better example, not involving subranges. Maybe Nim is working as intended, but as a new user of Nim, it seems strange.
# this is a nim test program
import tables
var
map = initTable[int, int]()
const print = echo
let
ani = 0
ani8 = 0i8
ani16 = 0i16
ani32 = 0i32
ani64 = 0i64
anu = 0u
anu8 = 0u8
anu16 = 0u16
anu32 = 0u32
anu64 = 0u64
print high(int) # prints 9223372036854775807
# the ## lines cause a type error with Nim 1.2.0
map[ani] = ani
map[ani8] = ani8
map[ani16] = ani16
map[ani32] = ani32
## map[ani64] = ani64
## map[anu] = anu
## map[anu8] = anu8
## map[anu16] = anu16
## map[anu32] = anu32
## map[anu64] = anu64
It seems like the only statement that should cause a type error is the last one, with an unsigned 64-bit integer, since a uint64 may not fit into an int64.
I'll be glad to open an issue if this behavior is not intended.
I did a little research and found an interesting post on mixing signed / unsigned issues, so I guess it's a good idea Nim doesn't like that: https://critical.eschertech.com/2010/04/07/danger-unsigned-types-used-here
It's still a little confusing to me that int64 isn't compatible with int on a 64-bit machine, and that it's necessary to add types to integer constants. I've seen that in Nim system code and am not too crazy about it, but there's probably a good reason for it that I just don't understand. I've never seen it much in other code except when doing right shifts to control whether signs extend or not.
And thanks @cblake for your post about hash(x) = x. I revised my test to actually hash the integers as you advised, and probably like Go does (though I haven't verified that):
import hashes
proc hash(x: int): Hash {.inline.} =
## This is from https://github.com/tommyettinger
var x = uint64(x)
x = x xor (x shr 27)
x = x * 0x3C79AC492BA7B653'u64
x = x xor (x shr 33)
x = x * 0x1C69B3F74AC4AE35'u64
x = x xor (x shr 27)
result = Hash(x)
import tables
const
n = 10_000_000
var
map = initTable[int, int]()
for i in 0..<n:
map[i] = i
for i in 0..<3:
for j in 0..<n:
map[j] = map[j] + 1
echo map[13]
ms:nim jim$ /usr/bin/time -l ./p3hash
16
3.69 real 3.46 user 0.22 sys
806027264 maximum resident set size
196793 page reclaims
9 page faults
1 voluntary context switches
5 involuntary context switches