Здравствуйте, Sinclair, Вы писали:
CC>>INC EAX выставит SF при знаковом переполнении, так что CMP там в общем то и не нужен.
S>Нужен. Если мы сделаем INC на -8 (0xFFFFFFF8), то получится 0xFFFFFFF9, и SF==1.
S>Прыжки вокруг флагов, инкрементов, и вычитаний нужны только оттого, что компилятор не может статически решить уравнение x+1<x.
Тут стоит задасться вопросом — а зачем компилятор вообще пытается решить такие "уравнения"?
Это из-за шаблонов, из-за сильного раздутия бинарников по мере того, как шаблонный код становился всё более популярным когда-то.
Такой подход позволяет обрезать из шаблонного кода ненужные ветки где только возможно дотянуться анализом кода.
Но оно же работает для любого инлайна.
В дотнете я как-то показывал трюк эмуляции числовых-константных параметров шаблонов:
https://godbolt.org/z/fKvE14G33
using System;
using System.Runtime.CompilerServices;
interface IYesNo {
bool Value { get; }
}
struct Yes : IYesNo {
public bool Value => true;
}
struct No : IYesNo {
public bool Value => false;
}
class SomeType<TConfig> where TConfig : struct, IYesNo
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Method() {
if(default(TConfig).Value) {
Console.WriteLine("Yes");
}
else {
Console.WriteLine("No");
}
}
}
class Program
{
static void Main() {
SomeType<Yes>.Method();
SomeType<No>.Method();
}
}
В примитивных случаях работает — уходит бранчинг, половина кода метода выбрасывается за ненадобностью.
Кстате, в 8-м дотнете в сравнении с 5-6-ми, где я в последний раз проверял этот трюк, наконец-то убрали ненужную двойную инициализацию временного значения default(TConfig), где сгенерённый код выглядел полнейшим нубством.
Вот для 8-го дотнета:
Program:Main() (FullOpts):
push rbp
mov rbp, rsp
mov rdi, 0x7F2C74BD06E8 ; 'Yes'
call [System.Console:WriteLine(System.String)]
mov rdi, 0x7F2C74BD0708 ; 'No'
call [System.Console:WriteLine(System.String)]
nop
pop rbp
ret
Вот для 6-го:
Program:Main():
push rbp
sub rsp, 16
lea rbp, [rsp+10H]
mov byte ptr [rbp-08H], 0
lea rdi, bword ptr [rbp-08H]
mov byte ptr [rdi], 0
mov rdi, qword ptr [(reloc)]
mov rdi, gword ptr [rdi]
call [System.Console:WriteLine(System.String)]
mov byte ptr [rbp-10H], 0
lea rdi, bword ptr [rbp-10H]
mov byte ptr [rdi], 0
mov rdi, qword ptr [(reloc)]
mov rdi, gword ptr [rdi]
call [System.Console:WriteLine(System.String)]
nop
add rsp, 16
pop rbp
ret
В общем, за оптимизацию дотнета явно взялись, и я это наблюдаю не только по этому трюку, просто не делаю полный список замеченного.
S>То есть для signed типов он решение находит (просто неверное с т.з. человеческой логики), а для unsigned — увы.
Для сравнения, дотнет ничего не ищет, тупо исполняет:
Program:IsMax(int):bool:
lea eax, [rdi+01H]
cmp eax, edi
setl al
movzx rax, al
ret
Забавно, что сложение выполняется в 64 бит, да еще через трюк адресной арифметики, но запоминаются младшие 32 бит результата.
S>Если бы мог, то он бы просто заменил всю арифметику на сравнение с единственным (для каждого типа) верным решением.
Для этого требуется сначала объявить переполнение знаковых не потенциальной ошибкой, а нормой.
Отрасль не поймёт. ))
К тому же, C# разрабатывается одной конторой, а для плюсов когда-то было большой победой, что куча независимых компиляторов стали понимать исходный код друг друга.
Плюс, некоторые UB довольно-таки, забавны.
Например, вызов метода объекта, когда не обращаются данным объекта, т.е. ни к виртуальному методу, ни к полю.
Например, в C#:
https://godbolt.org/z/rnaxjo4Kv
using System;
class SomeClass {
public void Method() {
Console.WriteLine("!!!");
}
}
class Program
{
static void Main()
{
SomeClass? sc = null;
var s = Console.ReadLine();
if(s.Length > 1)
sc = new SomeClass();
sc.Method();
}
}
Вызов sc.Method() полностью заинлайнился, обращения к this нет, но в коде перед вызовом метода всё-равно стоит проверка, что адрес sc валидный:
cmp byte ptr [rbx], bl
(если в rbx будет null, то сгенерируется прерывание AV)
А
sc!.Method() убирает лишь ворнинг.
В плюсах это UB, но, например, в MSVC прекрасно работает:
#include <iostream>
using namespace std;
struct SomeObj {
void Method() {
std::cout << "!!!" << std::endl;
}
};
int main()
{
int i;
cin >> i;
SomeObj * obj = nullptr;
if(i > 0)
obj = new SomeObj();
obj->Method();
return 0;
}
Скомпиллировано как написано, условно создаётся объект obj, вызов метода заинлайнился, обращения к this нет, хотя вызов метода через nullptr.
Clang безусловно создаёт ненужный объект, потом вызывает заинлайненное тело.
GCC безусловно вызывает заинлайненное тело, никогда не выделяя память в куче для ветки
obj = new SomeObj().
И кто тут прав?
C#, который делает лишнюю проверку в рантайм перед вызовом практически каждого метода (nullable-аннотация не помогает), хотя зачастую нет обращения к this?
Или MSVC, который исполняет ненужный бранчинг?
Или Clang, который создаёт ненужный объект?
Или GCC, который не создаёт ненужный объект, но даже косвенно про проблему узнать не получится, бо нет утечки памяти? ))