Let's go deep with const
in Dart and see how it works under the hood.
const
denotes a variable whose value is known at compile-time. The value of the variable cannot be overwritten during runtime and nor can the value change internally.
given the following Dart code:
import 'dart:io' show exit;
const intConst = 0xDEADBEEF;
void main(List<String> args) {
print(intConst);
exit(0);
}
we will get the following AOT compilation:
Precompiled____main_1558:
000000000005faec push rbp ; CODE XREF=Precompiled____main_main_1559+17
000000000005faed mov rbp, rsp
000000000005faf0 cmp rsp, qword [r14+0x40]
000000000005faf4 jbe loc_5fb17
loc_5fafa:
000000000005fafa mov eax, 0xdeadbeef ; CODE XREF=Precompiled____main_1558+50
000000000005faff push rax
000000000005fb00 call Precompiled____print_813 ; Precompiled____print_813
000000000005fb05 pop rcx
000000000005fb06 call Precompiled____exit_1070 ; Precompiled____exit_1070
000000000005fb0b mov rax, qword [r14+0xc8]
000000000005fb12 mov rsp, rbp
000000000005fb15 pop rbp
000000000005fb16 ret
; endp
loc_5fb17:
000000000005fb17 call qword [r14+0x240] ; CODE XREF=Precompiled____main_1558+8
000000000005fb1e jmp loc_5fafa
there are no surprises there, the const value is placed right inside the eax
32-bit register (not sure why we are using eax here and not rax
, could it be because it's faster?), and then pushed into the stack using the push
instruction, and then we are calling the Precompiled____print_813
procedure to print it to the console. The thing to note is how the value of that integer was placed in the stack (as opposed to the heap) and then printed immediately.
given the following Dart code:
import 'dart:io' show exit;
const doubleConst = 1.2;
void main(List<String> args) {
print(doubleConst);
exit(0);
}
we'll get the following AOT compiled code:
Precompiled____main_1558:
000000000005fac8 push rbp ; CODE XREF=Precompiled____main_main_1559+17
000000000005fac9 mov rbp, rsp
000000000005facc cmp rsp, qword [r14+0x40]
000000000005fad0 jbe loc_5faec
loc_5fad6:
000000000005fad6 call Precompiled____print_813 ; Precompiled____print_813, CODE XREF=Precompiled____main_1558+43
000000000005fadb call Precompiled____exit_1070 ; Precompiled____exit_1070
000000000005fae0 mov rax, qword [r14+0xc8]
000000000005fae7 mov rsp, rbp
000000000005faea pop rbp
000000000005faeb ret
; endp
loc_5faec:
000000000005faec call qword [r14+0x240] ; CODE XREF=Precompiled____main_1558+8
000000000005faf3 jmp loc_5fad6
this code is very different from the const int
variant, since you see nowhere in this code where the value of 1.2
is moved to any register of any sort. Instead, Dart has created a new function called Precompiled____print_813
and is calling that function instead. Let's go deep into that function to see what's going on:
Precompiled____print_813:
0000000000037458 push rbp ; CODE XREF=Precompiled____main_1558+14
0000000000037459 mov rbp, rsp
000000000003745c cmp rsp, qword [r14+0x40]
0000000000037460 jbe loc_37488
loc_37466:
0000000000037466 mov r11, qword [r15+0x20af] ; CODE XREF=Precompiled____print_813+55
000000000003746d push r11
000000000003746f call Precompiled__Double_0150898_toString_1175 ; Precompiled__Double_0150898_toString_1175
0000000000037474 pop rcx
0000000000037475 push rax
0000000000037476 call Precompiled____printToConsole_146 ; Precompiled____printToConsole_146
000000000003747b pop rcx
000000000003747c mov rax, qword [r14+0xc8]
0000000000037483 mov rsp, rbp
0000000000037486 pop rbp
0000000000037487 ret
; endp
loc_37488:
0000000000037488 call qword [r14+0x240] ; CODE XREF=Precompiled____print_813+8
000000000003748f jmp loc_37466
the first part of the code before the linebreak is the setting up of the local stack so I won't talk about that. the interesting part is the loc_37466
label where the first instruction is the mov
instruction. This is most definitely moving the pointer to the doubleConst
constat into the r11
64-bit register. I could be wrong about this but I think that's it. So Dart is not hardcoding the 1.2
value as a 64-bit floating point into a register, instead, it's loading it from an effective address into the r11
register. I think this could be less efficient than putting the constant value of the double
right into the stack but I could be wrong about this. If you know, let me know too!
then Dart is calling the Precompiled__Double_0150898_toString_1175
function, supposedly to convert the double to a string instance. I won't go into the internals of that function but we get what it's doing. once that function is done, it puts its result (the pointer to the string) into the rax
64-bit register nad we are then pushing rax
into the stack just before calling the Precompiled____printToConsole_146
function which will read that value from its stack using the rbp
and rsp
. so to summarize, const double
values are not really loaded into the stack in Dart the same way as const int
. I don't know why! They could be though, if they were just treated as a 64-bit value and placed right into a 64-bit register like rax
! If you're a compiler engineer at Google or know why this is not the case, please do chime in.
given the following Dart code:
import 'dart:io' show exit;
const stringConst = 'Hello, World!';
void main(List<String> args) {
print(stringConst);
exit(0);
}
we get the following AOT:
Precompiled____main_1558:
000000000005fab8 push rbp ; CODE XREF=Precompiled____main_main_1559+17
000000000005fab9 mov rbp, rsp
000000000005fabc cmp rsp, qword [r14+0x40]
000000000005fac0 jbe loc_5fadc
loc_5fac6:
000000000005fac6 call Precompiled____print_813 ; Precompiled____print_813, CODE XREF=Precompiled____main_1558+43
000000000005facb call Precompiled____exit_1070 ; Precompiled____exit_1070
000000000005fad0 mov rax, qword [r14+0xc8]
000000000005fad7 mov rsp, rbp
000000000005fada pop rbp
000000000005fadb ret
; endp
loc_5fadc:
000000000005fadc call qword [r14+0x240] ; CODE XREF=Precompiled____main_1558+8
000000000005fae3 jmp loc_5fac6
this is very similar to the const double
AOT if you look closely, so let's go deep into the Precompiled____print_813
function and see what's happening there:
000000000003745c push rbp ; CODE XREF=Precompiled____main_1558+14
000000000003745d mov rbp, rsp
0000000000037460 cmp rsp, qword [r14+0x40]
0000000000037464 jbe loc_3747b
loc_3746a:
000000000003746a call Precompiled____printToConsole_146 ; Precompiled____printToConsole_146, CODE XREF=Precompiled____print_813+38
000000000003746f mov rax, qword [r14+0xc8]
0000000000037476 mov rsp, rbp
0000000000037479 pop rbp
000000000003747a ret
; endp
loc_3747b:
000000000003747b call qword [r14+0x240] ; CODE XREF=Precompiled____print_813+8
0000000000037482 jmp loc_3746a
okay that was unexpected! the main function called this function and all this function is doing is just calling another function called Precompiled____printToConsole_146
and nowhere here call we find any reference to our string. I find this to be one layer too much, but I could be wrong here. let's see what's happening inside the Precompiled____printToConsole_146
function:
000000000000f2ec push rbp ; CODE XREF=Precompiled____print_813+14
000000000000f2ed mov rbp, rsp
000000000000f2f0 cmp rsp, qword [r14+0x40]
000000000000f2f4 jbe loc_f346
loc_f2fa:
000000000000f2fa mov rax, qword [r14+0x88] ; CODE XREF=Precompiled____printToConsole_146+97
000000000000f301 mov rax, qword [rax+0x38]
000000000000f305 cmp rax, qword [r15+0x27]
000000000000f309 jne loc_f31b
000000000000f30f mov rax, qword [r15+0x1867]
000000000000f316 call Precompiled_Stub__iso_stub_InitStaticFieldStub ; Precompiled_Stub__iso_stub_InitStaticFieldStub
loc_f31b:
000000000000f31b mov rcx, qword [rax+0x1f] ; CODE XREF=Precompiled____printToConsole_146+29
000000000000f31f push rax
000000000000f320 mov r11, qword [r15+0x20af]
000000000000f327 push r11
000000000000f329 mov rax, rcx
000000000000f32c mov r10, qword [r15+0x7f]
000000000000f330 mov rcx, qword [rax+0xf]
000000000000f334 call rcx
000000000000f336 pop r11
000000000000f338 pop r11
000000000000f33a mov rax, qword [r14+0xc8]
000000000000f341 mov rsp, rbp
000000000000f344 pop rbp
000000000000f345 ret
; endp
loc_f346:
000000000000f346 call qword [r14+0x240] ; CODE XREF=Precompiled____printToConsole_146+8
000000000000f34d jmp loc_f2fa
the part we're interested in is this:
loc_f31b:
000000000000f31b mov rcx, qword [rax+0x1f] ; CODE XREF=Precompiled____printToConsole_146+29
000000000000f31f push rax
000000000000f320 mov r11, qword [r15+0x20af]
000000000000f327 push r11
000000000000f329 mov rax, rcx
000000000000f32c mov r10, qword [r15+0x7f]
000000000000f330 mov rcx, qword [rax+0xf]
000000000000f334 call rcx
it's interesting that in the other cases, the print function was being called with a call function and just a reference to the label (function name) but in this case the pointer to the print function is being loaded into the 64-bit register ecx
(first line of code) and then called finally on the last line! the strange part is that [rax+0x1f]
is literally a pointer to 000000000000f309 jne loc_f31b
which is jump short if not equal (if zero flag is 0, check EFLAGS in Intel references). this is way above my head and I don't really understand what Precompiled_Stub__iso_stub_InitStaticFieldStub
does to be honest, but from the looks of it, it seems like it loads a given (through a push on the stack) static string from the data sector into the memory so that it can be used. But if you know better, please let me know too. Here is the data sector's placement of our string though if you can solve this mystery out yourself
0000000000084b30 db 0x48 ; 'H'
0000000000084b31 db 0x65 ; 'e'
0000000000084b32 db 0x6c ; 'l'
0000000000084b33 db 0x6c ; 'l'
0000000000084b34 db 0x6f ; 'o'
0000000000084b35 db 0x2c ; ','
0000000000084b36 db 0x20 ; ' '
0000000000084b37 db 0x57 ; 'W'
0000000000084b38 db 0x6f ; 'o'
0000000000084b39 db 0x72 ; 'r'
0000000000084b3a db 0x6c ; 'l'
0000000000084b3b db 0x64 ; 'd'
0000000000084b3c db 0x21 ; '!'
with the data section header reading as follows:
; Segment Segment 6
; Range: [0x62000; 0xb2840[ (329792 bytes)
; File offset : [401408; 731200[ (329792 bytes)
; Permissions: readable
; Flags: 0x4
; Section .rodata
; Range: [0x62000; 0xb26f0[ (329456 bytes)
; File offset : [401408; 730864[ (329456 bytes)
; Flags: 0x2
; SHT_PROGBITS
; SHF_ALLOC
_kDartIsolateSnapshotData:
0000000000062000 db 0xf5 ; '.'
0000000000062001 db 0xf5 ; '.'
0000000000062002 db 0xdc ; '.'
given the following Dart code:
import 'dart:io' show exit;
const intConst = 0xDEADBEEF;
const doubleConst = 1.2;
void main(List<String> args) {
print(intConst);
print(doubleConst);
exit(0);
}
i would expect the intConst
to be loaded into eax
as it was before in the previous section where we talked about constant integers. but that's not the case! let's look at the AOT:
Precompiled____main_1558:
000000000005fad8 push rbp ; CODE XREF=Precompiled____main_main_1559+17
000000000005fad9 mov rbp, rsp
000000000005fadc cmp rsp, qword [r14+0x40]
000000000005fae0 jbe loc_5fb15
loc_5fae6:
000000000005fae6 mov r11, qword [r15+0x207f] ; CODE XREF=Precompiled____main_1558+68
000000000005faed push r11
000000000005faef call Precompiled____print_813 ; Precompiled____print_813
000000000005faf4 pop rcx
000000000005faf5 mov r11, qword [r15+0x2087]
000000000005fafc push r11
000000000005fafe call Precompiled____print_813 ; Precompiled____print_813
000000000005fb03 pop rcx
000000000005fb04 call Precompiled____exit_1070 ; Precompiled____exit_1070
000000000005fb09 mov rax, qword [r14+0xc8]
000000000005fb10 mov rsp, rbp
000000000005fb13 pop rbp
000000000005fb14 ret
; endp
loc_5fb15:
000000000005fb15 call qword [r14+0x240] ; CODE XREF=Precompiled____main_1558+8
000000000005fb1c jmp loc_5fae6
now all of a sudden the intConst
is not placed in the eax
register anymore, instead it is loaded like this:
000000000005fae6 mov r11, qword [r15+0x207f] ; CODE XREF=Precompiled____main_1558+68
000000000005faed push r11
000000000005faef call Precompiled____print_813 ; Precompiled____print_813
this is pretty much another way of saying:
stack[-16] = *(r15 + 0x207f);
Precompiled____print_813(rdi, rsi, rdx, rcx, r8, r9, stack[-16]);
so this way we are loading the pointer to the intConst
into the stack and then calling the Precompiled____print_813
function with that value placed in the stack. the intConst
got demoted from a constant register value to a stack value for some reason. I think only a dart compiler engineer at Google can answer why this demotion happened to be honest. If you know the answer please let me know.
let's have a look at a constant custom class:
import 'dart:io' show exit;
class Person {
final int age;
const Person(this.age);
}
void main(List<String> args) {
final foo = Person(0xDEADBEEF);
print(foo.age);
exit(0);
}
that compiles to the following AOT:
000000000005faec push rbp ; CODE XREF=Precompiled____main_main_1559+17
000000000005faed mov rbp, rsp
000000000005faf0 cmp rsp, qword [r14+0x40]
000000000005faf4 jbe loc_5fb17
loc_5fafa:
000000000005fafa mov eax, 0xdeadbeef ; CODE XREF=Precompiled____main_1558+50
000000000005faff push rax
000000000005fb00 call Precompiled____print_812 ; Precompiled____print_812
000000000005fb05 pop rcx
000000000005fb06 call Precompiled____exit_1066 ; Precompiled____exit_1066
000000000005fb0b mov rax, qword [r14+0xc8]
000000000005fb12 mov rsp, rbp
000000000005fb15 pop rbp
000000000005fb16 ret
; endp
loc_5fb17:
000000000005fb17 call qword [r14+0x240] ; CODE XREF=Precompiled____main_1558+8
000000000005fb1e jmp loc_5fafa
this is great, and just as you'd expect it. the 32-bit value of 0xdeadbeef
that was assigned to the age
of the Person
instance is placed right into the eax
32-bit register and then printed to the screen. nice, no instance of the Person
class was even created.
now let's make it more complicated and have some logic in the initializer of the Person
class:
import 'dart:io' show exit;
class Person {
final int age;
const Person(int age) : this.age = age + 0xFEEDFEED;
}
void main(List<String> args) {
final foo = Person(0xDEADBEEF);
print(foo.age);
exit(0);
}
and get the following AOT:
Precompiled____main_1558:
000000000005faec push rbp ; CODE XREF=Precompiled____main_main_1559+17
000000000005faed mov rbp, rsp
000000000005faf0 cmp rsp, qword [r14+0x40]
000000000005faf4 jbe loc_5fb1c
loc_5fafa:
000000000005fafa movabs rax, 0x1dd9bbddc ; CODE XREF=Precompiled____main_1558+55
000000000005fb04 push rax
000000000005fb05 call Precompiled____print_812 ; Precompiled____print_812
000000000005fb0a pop rcx
000000000005fb0b call Precompiled____exit_1066 ; Precompiled____exit_1066
000000000005fb10 mov rax, qword [r14+0xc8]
000000000005fb17 mov rsp, rbp
000000000005fb1a pop rbp
000000000005fb1b ret
; endp
loc_5fb1c:
000000000005fb1c call qword [r14+0x240] ; CODE XREF=Precompiled____main_1558+8
000000000005fb23 jmp loc_5fafa
this is also really neat. what Dart did here was that it took 0xdeadbeef
and found out that the constructor of the Person
class is doing some calculation with that value, which in this case is to add 0xfeedfeed
to it and if you calculate that yourself you'll get 0x1DD9BBDDC
and it placed that value directly in the rax
32-bit register and passed it to the print statement. Neat huh?
let's make the Person
class more complicated and exciting:
import 'dart:io' show exit;
class Person {
final String firstName;
final String lastName;
final String fullName;
const Person(this.firstName, this.lastName) : fullName = '$firstName $lastName';
}
void main(List<String> args) {
final foo = Person('Foo', 'Bar');
print(foo);
exit(0);
}
to get the following AOT:
Precompiled____main_1558:
000000000005fac0 push rbp ; CODE XREF=Precompiled____main_main_1560+17
000000000005fac1 mov rbp, rsp
000000000005fac4 cmp rsp, qword [r14+0x40]
000000000005fac8 jbe loc_5faeb
loc_5face:
000000000005face call Precompiled_AllocationStub_Person_1559 ; Precompiled_AllocationStub_Person_1559, CODE XREF=Precompiled____main_1558+50
000000000005fad3 push rax
000000000005fad4 call Precompiled____print_812 ; Precompiled____print_812
000000000005fad9 pop rcx
000000000005fada call Precompiled____exit_1066 ; Precompiled____exit_1066
000000000005fadf mov rax, qword [r14+0xc8]
000000000005fae6 mov rsp, rbp
000000000005fae9 pop rbp
000000000005faea ret
; endp
loc_5faeb:
000000000005faeb call qword [r14+0x240] ; CODE XREF=Precompiled____main_1558+8
000000000005faf2 jmp loc_5face
oh, spicy! now we got some juice out of the example. you can see how the first line of the loc_5face
label is making a call to the Precompiled_AllocationStub_Person_1559
procedure. In this case, since our example got more complicated and involves two String
final values to construct the eventual instance of the Person
class, Dart doesn't seem to be able to optimize the implementation since print(foo)
will internally call the toString()
function of Person
which we haven't overwritten, so it will have to make an instance of the Person
class which is an Object
and internally call that function to print out the toString()
result. lots going on here but one thing at a time, let's look at Precompiled_AllocationStub_Person_1559
and see what's going on there:
Precompiled_AllocationStub_Person_1559:
000000000005faf4 mov r8d, 0xc70104 ; CODE XREF=Precompiled____main_1558+14
000000000005fafa jmp qword [r14+0x228]
; endp
which is another interpretation of the following pseudo-code:
void Precompiled_AllocationStub_Person_1559() {
(*(r14 + 0x228))();
return;
}
since I am not debuggin the code while looking at the code, I can't be 100% sure what's lurking inside the (*(r14 + 0x228))
pointer but it's for sure a function since it is being jumped to using the jmp
instruction. the double-word register of r8d is being set to 0xc70104
though so that could be the parameter to the function lurking behind the pointer. this could all be the internals of Dart OR I may just not know what I'm talking about to be honest but for me this seems like an allocation function that is hidden from plain-sight and is creating a new instance of the Person
class and placing its pointer in the eax
register since if you look again at the original asm code, you'll see this:
000000000005fad3 push rax
000000000005fad4 call Precompiled____print_812 ; Precompiled____print_812
so here eax
is for sure the pointer to our Person
instance which then gets sent to Precompiled____print_812
which internally calls the toString()
function on that instance.
the const
syntax in Dart is, like other languages, a hint to the compiler to make both your life and the compiler's life easier. some calculations might be done at compile-time to even make the code run faster such as our Person
class above where we only stored the person's age and then printed that age property directly to the screen. No Person
instance was then created in that case. But when we made the Person
class carry more information and have a computed property, things got more complicated. Here are a few takeaways:
- constant
int
are sometimes placed inside a register (not even in the stack) directly and then worked with. as shown in this articleint
constants can be demoted to stack variables in some certain conditions and I don't really know the reason why! - constant
double
values are loaded from memory (not placed directly inside a register, unlike constantint
values) and then used - constant
String
instances are first loaded into the memory through 2 layers of function calls and then printed to the screen - constant custom class instances, if just placeholders for data, and depending on what you are doing with those instances, could simply be calculated at compile-time and placed inside CPU registers to be used.
- constant custom class instances, if more complicated and doing calculations such as string concatenation, and depending on what you do with those instances, may need to be allocated at run-time into their instances, resolved to proper pointers in the heap, and then passed around to different functions to be processed.
- Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 1: Basic Architecture
- Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z