[C++] Assembly Differences with deducing this
I recently discovered that MSVC gives slightly different assembly when using deducing this
instead of the implicit this
pointer.
The following function is the string scanning function from my compiler’s scanner.
The function loops until it finds the second "
or it reaches the end of the file.
Escape characters are ignored.
auto Scanner::string() -> SourceToken {
while (true) {
switch (peek()) {
case '\\': {
i_offset += 2;
break;
}
case '"': {
i_offset++;
goto loop_end;
}
case '\0': {
return make_valueless_token(TokenType::END_OF_FILE);
}
default: {
i_offset++;
break;
}
}
}
loop_end:
// Offset to remove quote characters
auto const without_quotes{this->file.substr(i + 1, i_offset - 1)};
return make_value_token(TokenType::STRING, without_quotes, without_quotes);
}
With C++23’s deducing this
feature, we can also explicitly list the class instance as an object parameter.
The fragment below illustrates this.
auto Scanner::string(this Scanner& self) -> SourceToken {
while (true) {
switch (self.peek()) {
}
Here is MSVC’s output with *this
.
I’m only showing the instructions in the main loop for brevity.
; 835 : auto Scanner::string() -> SourceToken {
$LN110:
push rbx
sub rsp, 64 ; Initialise the stack
mov r10, QWORD PTR [rcx] ; r10 = this->i
mov rbx, rdx ;
mov r11, QWORD PTR [rcx+32] ; r11 = this->file_size
$LL2@string: ; Start of while loop
; [Peek the next char]
mov r9, QWORD PTR [rcx+8] ; r9 = this->i_offset
lea r8, QWORD PTR [r9+1] ; i_offset++
add r8, r10 ; r8 = (i_offset + 1) + i
cmp r8, r11 ; Check if EOF
jae $LN10@string ; goto EOF handler
mov rax, QWORD PTR [rcx+16] ; rax = this->file.data_
movzx r8d, BYTE PTR [r8+rax] ; r8d = this->file[i + i_offset + 1]
test r8b, r8b ; c == \0
je $LN10@string ; goto EOF handler
lea rdx, QWORD PTR [r9+1] ; rdx = i_offset+1
mov QWORD PTR [rcx+8], rdx ; Update this->i_offset
cmp r8b, 34 ; Check "
je SHORT $LN8@string ; goto " handler
cmp r8b, 92 ; Check \
jne SHORT $LL2@string ; Restart loop if c != \
; \ Handler
lea r8, QWORD PTR [r10+1] ; r8 = i+1
add r8, rdx ; r8 = i + i_offset + 2
cmp r8, r11 ; Check if EOF
jae SHORT $LL2@string ; Next iter if !EOF
cmp BYTE PTR [r8+rax], 34 ; Check "
jne SHORT $LL2@string ; Next iter if !"
lea rax, QWORD PTR [rdx+1] ; i_offset++
mov QWORD PTR [rcx+8], rax ; Update this->i_offset
jmp SHORT $LL2@string ; Next iter
$LN8@string:
$loop_end$111:
The output with self
.
; 835 : auto Scanner::string(this Scanner& self) -> SourceToken {
$LN96:
push rbx
sub rsp, 64 ; Initialise the stack
mov r9, QWORD PTR [rdx] ; r9 = i
mov rax, rdx ; rax = &self
mov r10, QWORD PTR [rdx+32] ; r10 = self.file_size
mov rbx, rcx ; ???
npad 13
$LL2@string: ; Start of loop
; [Peek the next char]
mov r8, QWORD PTR [rax+8] ; r8 = self.i_offset
lea rdx, QWORD PTR [r9+1] ; rdx = i + 1
add rdx, r8 ; rdx = i_offset + (i + 1)
cmp rdx, r10 ; Check EOF
jae $LN9@string ; goto EOF handler
mov rcx, QWORD PTR [rax+16] ; rcx = self.file.data_
movzx edx, BYTE PTR [rdx+rcx] ; edx = file[i_offset + i + 1]
test dl, dl ; rdx == \0
je $LN9@string ; goto EOF handler
cmp dl, 34 ; c == "
je SHORT $LN7@string ; goto " handler
cmp dl, 92 ; c == \
je SHORT $LN6@string ; goto \ handler
inc r8 ; i_offset++
mov QWORD PTR [rax+8], r8 ; Update self.i_offset
jmp SHORT $LL2@string ; Next iter
$LN6@string: ; Handle \
add r8, 2 ; i_offset += 2
mov QWORD PTR [rax+8], r8 ; Update self.i_offset
jmp SHORT $LL2@string ; Next iter
$LN7@string: ; Handle "
lea rcx, QWORD PTR [r8+1] ; rcx = i_offset + 1
mov QWORD PTR [rax+8], rcx ; Update self.i_offset
$loop_end$97: ; End of the loop
The output with self
seems easier to follow.
It doesn’t check for "
twice and the jumps are more logical e.g. go to the \
handling block immediately after checking that the character is a \
.