Всем привет!
Может кто занимался написанием z-интерполяции в треугольнике?
Очень хочется избавиться от деления единицы на произведение векторов.
+ Показать
− Скрыть
Z := 1.0 / (Pt.X * Z.X + Pt.Y * Z.Y + Pt.Z * Z.Z);
Pt.X/Y/Z - барицентрические координаты точки в треугольнике.
Z.X/Y/Z - вектор глубины треугольника, т.е. единица поделенная на Z вершин.
Есть идеи или рекомендации по этому поводу?
Лучше одно доп. умножение, чем деление.
Убрав единицу в итоге неплохо поднимается ФПС в софтрендере.
Можно попробовать заменить деление на поиск приближенного обратного числа. Ни чего другого тут не изменишь. Максимум надо смотреть всю реализацию, возможно что то переставить, чтобы делений было не так много.
eDmk
> произведение векторов.
Это скалярное произведение, используется для проекции вектора на ось Z.
Функция пересчета трехмерных координат в проекцию у меня тоже имеет одно деление и я особенно не парился. Только это деление используется для нормализации значений под размеры проекционного куба, а не для расчета глубины. То есть если убрать деление то, остальные расчеты нужно будет производить уже с учетом домножения на r[3] и последующим делением на r[3]. А иначе картинка будет просто ортогональной, а не перспективной.
+ Показать
− Скрыть
_int32 _APICALL _V3FProjDEF(float *_a,void *_b,float *_c,float *_d,float *_e) {
float *_matrixa=(float *)_b;
float r[5];
r[0]=_a[0]*_matrixa[0]+_a[1]*_matrixa[4]+_a[2]*_matrixa[8]+_matrixa[12];
r[1]=_a[0]*_matrixa[1]+_a[1]*_matrixa[5]+_a[2]*_matrixa[9]+_matrixa[13];
r[2]=_a[0]*_matrixa[2]+_a[1]*_matrixa[6]+_a[2]*_matrixa[10]+_matrixa[14];
r[3]=_a[0]*_matrixa[3]+_a[1]*_matrixa[7]+_a[2]*_matrixa[11]+_matrixa[15];
r[4]=-r[3];
_int32 flag=0;
if (r[0]>r[3]) flag|=0x01;
if (r[1]>r[3]) flag|=0x02;
if (r[2]>r[3]) flag|=0x04;
if (r[0]<r[4]) flag|=0x10;
if (r[1]<r[4]) flag|=0x20;
if (r[2]<r[4]) flag|=0x40;
if ((r[3]!=0) && (r[3]!=1.0)) {
r[3]=1.0f/r[3];
r[0]=r[0]*r[3];
r[1]=r[1]*r[3];
r[2]=r[2]*r[3];
}
_e[0]=r[0]*_c[0]+_d[0];
_e[1]=r[1]*_c[1]+_d[1];
_e[2]=r[2]*_c[2]+_d[2];
_e[3]=r[3];
return flag;
}
Да у меня 2 формулы перспективной интерполяции и везде надо единицу делить на выражение.
Видимо из-за перспективной матрицы, которая тоже приведена к единице.
Жаль. Это деление тормозит расчеты довольно сильно.
Обратное число это тоже самое число только в минус первой степени. Если посмотреть на вычисление быстрого обратного корня то
i = 0x7F800000 - ( i );
Будет первым приближением обратного числа. А дальше можно уточнить его по такой формуле:
y = y * ( 1.5 - ( x2 * y * y ) );
Или просто написать на асме с использованием соответствующей команды для вычисления обратного значения. Хотя на сколько я помню это делается автоматически компилятором, если используется формат float.
У меня софтверный треугольник рисуется так
+ Показать
− Скрыть
void _APICALL _T4AFSdrawDEF(V4AFdata *vertexs,T4AFdata *data)
{
V4AFdata *t;
V4AFdata *vt[3]={vertexs,vertexs+1,vertexs+2};
V4AFdata *vx1=vertexs;
V4AFdata *vx2=vertexs+1;
V4AFdata *vx3=vertexs+2;
_TriangleSort(vt);
tri_loop2 loop;
loop.cscale=0.0000152587890625;
int rv[3];
float rvf[3] = { (vt[0]->v[0] + 0.5f), (vt[1]->v[0] + 0.5f), (vt[2]->v[0] + 0.5f) };
vecFunctions.V3FTTo3I(rvf, rv);
int maxx = rv[0];
int minx = rv[0];
if (maxx<rv[1]) maxx = rv[1];
if (minx>rv[1]) minx = rv[1];
if (maxx<rv[2]) maxx = rv[2];
if (minx>rv[2]) minx = rv[2];
int maxy = matFunctions.FTrunc(vt[2]->v[1] + 0.5f);
int miny = matFunctions.FTrunc(vt[0]->v[1] + 0.5f);
int midly = matFunctions.FTrunc(vt[1]->v[1] + 0.5f);
vecFunctions.V4FTo4B(vt[0]->c,&loop.color);
if (minx<0) minx=0;
if (maxx>data->width-1) maxx=data->width-1;
if (maxx<minx) maxx=minx;
if (minx>maxx) minx=maxx;
if (miny<0) miny=0;
if (maxy>data->height-1) maxy=data->height-1;
if (maxy<miny) maxy=miny;
if (miny>maxy) miny=maxy;
if (midly<miny) midly=miny;
if (midly>maxy) midly=maxy;
loop.startmem=(char*)data->mem+miny*data->stride+(minx<<2);
loop.sad=data->stride;
//color
float normal0[3];
float normal1[3];
float normal2[3];
float normal3[3];
bt::vector3f pv1(vx1->v[0],vx1->v[1],vx1->c[0]);
bt::vector3f pv2(vx2->v[0],vx2->v[1],vx2->c[0]);
bt::vector3f pv3(vx3->v[0],vx3->v[1],vx3->c[0]);
vecFunctions.V3FSub(&pv2,&pv1,&pv2);
vecFunctions.V3FSub(&pv3,&pv1,&pv3);
vecFunctions.V3FCross(&pv2,&pv3,&normal0);
pv2.z=vx2->c[1]-vx1->c[1];
pv3.z=vx3->c[1]-vx1->c[1];
vecFunctions.V3FCross(&pv2,&pv3,&normal1);
pv2.z=vx2->c[2]-vx1->c[2];
pv3.z=vx3->c[2]-vx1->c[2];
vecFunctions.V3FCross(&pv2,&pv3,&normal2);
pv2.z=vx2->c[3]-vx1->c[3];
pv3.z=vx3->c[3]-vx1->c[3];
vecFunctions.V3FCross(&pv2,&pv3,&normal3);
float cys[4];
cys[0]=-normal0[1]/normal0[2];
cys[1]=-normal1[1]/normal1[2];
cys[2]=-normal2[1]/normal2[2];
cys[3]=-normal3[1]/normal3[2];
float cxs[4];
cxs[0]=-normal0[0]/normal0[2];
cxs[1]=-normal1[0]/normal1[2];
cxs[2]=-normal2[0]/normal2[2];
cxs[3]=-normal3[0]/normal3[2];
float s=(minx-vt[0]->v[0])*65536.0f;
vecFunctions.V4FCombine(vt[0]->c,cxs,loop.cstart,65536.0f,s);
vecFunctions.V4Fmul2NTrunc(cys,loop.cy,16);
vecFunctions.V4Fmul2NTrunc(cxs,loop.cx,16);
s=(miny-vt[0]->v[1])*65536.0f;
vecFunctions.V4FCombine(loop.cstart,cys,loop.cstart,1.0f,s);
vecFunctions.V4FTTo4I(loop.cstart,loop.cstart);
loop.cstart[0]-=32768;
loop.cstart[1]-=32768;
loop.cstart[2]-=32768;
loop.cstart[3]-=32768;
bool inv=false;
if (vecFunctions.V2FQDFL(vt[0]->v,vt[1]->v,vt[2]->v)>=0) {
t=vt[1];
vt[1]=vt[2];
vt[2]=t;
inv=true;
}
float vr1[3];
_calcplane(vt[0]->v,vt[1]->v,vr1);
float vr2[3];
_calcplane(vt[1]->v,vt[2]->v,vr2);
float vr3[3];
_calcplane(vt[2]->v,vt[0]->v,vr3);
float v[2]={minx+0.4999f,miny+0.5f};
loop.reg1[0]=(vr1[2]+v[1]*vr1[1]+v[0]*vr1[0]);
loop.reg1[1]=(vr3[2]+v[1]*vr3[1]+v[0]*vr3[0]);
loop.reg1[2]=(vr2[2]+v[1]*vr2[1]+v[0]*vr2[0]);
loop.reg2[0]=1.0f/vr1[0];
loop.reg2[1]=1.0f/vr3[0];
loop.reg2[2]=1.0f/vr2[0];
loop.reg3[0]=vr1[0];
loop.reg3[1]=vr3[0];
loop.reg3[2]=vr2[0];
loop.reg4[0]=vr1[1];
loop.reg4[1]=vr3[1];
loop.reg4[2]=vr2[1];
loop.range=maxx-minx;
loop.loopsize=midly-miny-1;
if (loop.loopsize>0) loop.reg1[2]+=loop.reg4[2]*loop.loopsize;
triangle_MultyColor_8888_loop2_deff(&loop);
loop.loopsize=1;
middle_MultyColor_8888_loop2_deff(&loop);
if (!inv) {
loop.reg1[0]=loop.reg1[1];
loop.reg2[0]=loop.reg2[1];
loop.reg3[0]=loop.reg3[1];
loop.reg4[0]=loop.reg4[1];
}
loop.reg1[1]=loop.reg1[2];
loop.reg2[1]=loop.reg2[2];
loop.reg3[1]=loop.reg3[2];
loop.reg4[1]=loop.reg4[2];
loop.loopsize=maxy-midly;
triangle_MultyColor_8888_loop2_deff(&loop);
}
Как видишь тут полно делений.
eDmk
XYZ - это индексы вершин? При этом начальная Z - это именно координата?
Не так страшно деление, как его малюют. Инструкция приблизительного деления (rcpss) примерно в ту же скорость что и умножение. Даже полноценное деление не такое уж долгое. А ещё можно 4 деления одновременно, с SIMD.
В кваке вроде делили раз в несколько пикселей, в посередине использовали результат от соседей.
Pt.X * Z.X + Pt.Y * Z.Y + Pt.Z * Z.Z можно без умножений, т. к. результат между соседними пикселями в строке отличается на константу.
Не обязательно использовать 3 барицентрические координаты: Pt.Z=1-(Pt.X+Pt.Y). Подставить, преобразовать.
Если у тебя паскаль - хз как там с оптимизациями (и 32-битными флоатами и SIMD). Можешь поспрашивать у тех, кто с этим больше работает.
Просто если заменить на умножение, то получается быстрее на 5 fps на ядро.
В совокупности на 4-х ядернике это почти + ~15 fps. На моем 10-и ядернике + ~30 fps.
Для софтрендера это хороший прирост. Вот и заморочился вопросом.
Да на асме все и написано. Все работает достаточно быстро, но без деления еще быстрее.
Правда результат пока неправильный.
+ Показать
− Скрыть
mulsd xmm0, Z.Z
mulsd xmm1, Z.Y
mulsd xmm2, Z.X
addsd xmm1, xmm0
addsd xmm1, xmm2
movq xmm0, R13
divsd xmm0, xmm1
cvtsd2ss xmm0, xmm0
>Не обязательно использовать 3 барицентрические координаты
К сожалению обязательно. Иначе треугольник неправильный.
eDmk
>К сожалению обязательно. Иначе треугольник неправильный.
Можно подробнее?
Pt.X * Z.X + Pt.Y * Z.Y + Pt.Z * Z.Z = Pt.X * Z.X + Pt.Y * Z.Y + (1.0-(Pt.X+Pt.Y)) * Z.Z = Z.Z + Pt.X * (Z.X - Z.Z) + Pt.Y * (Z.Y - Z.Z).
(Z.X - Z.Z) и (Z.Y - Z.Z) - константы.
Или у тебя Pt.X + Pt.Y + Pt.Z = 1.0 не выполняется? А почему?
//Делим единицу на выражение
divsd xmm0, xmm1
//Конвертируем в single
cvtsd2ss xmm0, xmm0
Нафига? Деление float32 заметно быстрее, чем деление float64.
Можно
//Конвертируем в single
cvtsd2ss xmm0, xmm0
//Делим единицу на выражение
divss xmm0, xmm1
или
//Конвертируем в single
cvtsd2ss xmm0, xmm0
//Делим единицу на выражение
rcpss xmm0, xmm0
если точности 14 бит достаточно.
И вообще нафига тебе double?
И можно считать 4 за раз (divps/rcpps).
Выражение Pt.X * Z.X + Pt.Y * Z.Y + Pt.Z * Z.Z меняется линейно вдоль строчки пикселей. Инкремент можно посчитать один раз, и дальше складывать с предыдущим результатом. Одно сложение, а не 3 умножения ж.
Тут правда без глубины.
+ Показать
− Скрыть
void triangle_MultyColor_8888_loop2_deff(tri_loop2 *inf)
{
while (inf->loopsize>0) {
_int32 t1=matFunctions.FTrunc(-(inf->reg1[0]*inf->reg2[0]));
_int32 t2 = matFunctions.FTrunc(-(inf->reg1[1] * inf->reg2[1]));
float inc1=(t1*inf->reg3[0]+inf->reg1[0]);
float dec2=(t2*inf->reg3[1]+inf->reg1[1]);
if (t1>t2) {
_int32 yt=t1;
t1=t2;
t2=yt;
float et=inc1;
inc1=dec2;
dec2=et;
}
if (t1<0) t1=0;
if (t2>inf->range) t2=inf->range;
if (inc1>0) t1++;
if (dec2>0) t2--;
vecFunctions.V4IScale(inf->cx,inf->colorr,(float)t1);
vecFunctions.V4IAdd(inf->cstart,inf->colorr,inf->colorr);
inf->count=t2-t1+1;
inf->mem=inf->startmem+(t1<<2);
if (inf->count>0) renFunctions.S4AFSdraw(inf);
vecFunctions.V4IAdd(inf->cstart,inf->cy,inf->cstart);
vecFunctions.V2FAdd(inf->reg1,inf->reg4,inf->reg1);
inf->startmem+=inf->sad;
inf->loopsize--;
}
}
void _APICALL _S4AFSdrawDEF(void *data)
{
if (((S4AFDSdata*)data)->cscale==0.00390625) {
while (((S4AFDSdata*)data)->count) {
((S4AFDSdata*)data)->color=0xFFFFFFFF;
if (((S4AFDSdata*)data)->color3<=65535) ((unsigned char*)&((S4AFDSdata*)data)->color)[3]=(unsigned char)(((S4AFDSdata*)data)->color3>>8);
if (((S4AFDSdata*)data)->color2<=65535) ((unsigned char*)&((S4AFDSdata*)data)->color)[2]=(unsigned char)(((S4AFDSdata*)data)->color2>>8);
if (((S4AFDSdata*)data)->color1<=65535) ((unsigned char*)&((S4AFDSdata*)data)->color)[1]=(unsigned char)(((S4AFDSdata*)data)->color1>>8);
if (((S4AFDSdata*)data)->color0<=65535) ((unsigned char*)&((S4AFDSdata*)data)->color)[0]=(unsigned char)(((S4AFDSdata*)data)->color0>>8);
*(_int32*)((S4AFDSdata*)data)->mem=((S4AFDSdata*)data)->color;
((S4AFDSdata*)data)->color3+=((S4AFDSdata*)data)->color3step;
((S4AFDSdata*)data)->color2+=((S4AFDSdata*)data)->color2step;
((S4AFDSdata*)data)->color1+=((S4AFDSdata*)data)->color1step;
((S4AFDSdata*)data)->color0+=((S4AFDSdata*)data)->color0step;
((S4AFDSdata*)data)->mem+=4;
((S4AFDSdata*)data)->count--;
}
return;
}
float max=255.0f/((S4AFDSdata*)data)->cscale;
float cs=((S4AFDSdata*)data)->cscale;
while (((S4AFDSdata*)data)->count) {
((S4AFDSdata*)data)->color=0xFFFFFFFF;
if (((S4AFDSdata*)data)->color3<=max) ((unsigned char*)&((S4AFDSdata*)data)->color)[3]=(unsigned char)(((S4AFDSdata*)data)->color3*cs);
if (((S4AFDSdata*)data)->color2<=max) ((unsigned char*)&((S4AFDSdata*)data)->color)[2]=(unsigned char)(((S4AFDSdata*)data)->color2*cs);
if (((S4AFDSdata*)data)->color1<=max) ((unsigned char*)&((S4AFDSdata*)data)->color)[1]=(unsigned char)(((S4AFDSdata*)data)->color1*cs);
if (((S4AFDSdata*)data)->color0<=max) ((unsigned char*)&((S4AFDSdata*)data)->color)[0]=(unsigned char)(((S4AFDSdata*)data)->color0*cs);
*(_int32*)((S4AFDSdata*)data)->mem=((S4AFDSdata*)data)->color;
((S4AFDSdata*)data)->color3+=((S4AFDSdata*)data)->color3step;
((S4AFDSdata*)data)->color2+=((S4AFDSdata*)data)->color2step;
((S4AFDSdata*)data)->color1+=((S4AFDSdata*)data)->color1step;
((S4AFDSdata*)data)->color0+=((S4AFDSdata*)data)->color0step;
((S4AFDSdata*)data)->mem+=4;
((S4AFDSdata*)data)->count--;
}
}
С глубиной так
+ Показать
− Скрыть
struct S4AFDSdata {
_int32 count;
_int32 color;
char *mem;
char *depth;
_int32 z;
_int32 za;
float zscale;
float cscale;
_int32 color0;
_int32 color1;
_int32 color2;
_int32 color3;
_int32 color0step;
_int32 color1step;
_int32 color2step;
_int32 color3step;
};
void _APICALL _S4AFDLSdrawDEF(void *data)
{
while (((S4AFDSdata*)data)->count) {
unsigned short s=(unsigned short)(((S4AFDSdata*)data)->z>>15);
if ((*(unsigned short*)((S4AFDSdata*)data)->depth) > s) {
((S4AFDSdata*)data)->color=0xFFFFFFFF;
if (((S4AFDSdata*)data)->color3<=65535) ((unsigned char*)&((S4AFDSdata*)data)->color)[3]=(unsigned char)(((S4AFDSdata*)data)->color3>>8);
if (((S4AFDSdata*)data)->color2<=65535) ((unsigned char*)&((S4AFDSdata*)data)->color)[2]=(unsigned char)(((S4AFDSdata*)data)->color2>>8);
if (((S4AFDSdata*)data)->color1<=65535) ((unsigned char*)&((S4AFDSdata*)data)->color)[1]=(unsigned char)(((S4AFDSdata*)data)->color1>>8);
if (((S4AFDSdata*)data)->color0<=65535) ((unsigned char*)&((S4AFDSdata*)data)->color)[0]=(unsigned char)(((S4AFDSdata*)data)->color0>>8);
*(_int32*)((S4AFDSdata*)data)->mem=((S4AFDSdata*)data)->color;
*(unsigned short*)((S4AFDSdata*)data)->depth=s;
}
((S4AFDSdata*)data)->color3+=((S4AFDSdata*)data)->color3step;
((S4AFDSdata*)data)->color2+=((S4AFDSdata*)data)->color2step;
((S4AFDSdata*)data)->color1+=((S4AFDSdata*)data)->color1step;
((S4AFDSdata*)data)->color0+=((S4AFDSdata*)data)->color0step;
((S4AFDSdata*)data)->z+=((S4AFDSdata*)data)->za;
((S4AFDSdata*)data)->mem+=4;
((S4AFDSdata*)data)->depth+=2;
((S4AFDSdata*)data)->count--;
}
}
_S4AFDLSdrawDEF и _S4AFSdrawDEF рисуют горизонтальную линию для разных режимов, с тестом глубины и без.
Тут интерполяция представлена в целых числах как числа с фиксированной точкой. Для цвета 8 бит после запятой, для глубины 15 бит после запятой. Буфер глубины 16 битный. Глубина для следующей точки как и цвет вычисляется одним сложением.
((S4AFDSdata*)data)->z+=((S4AFDSdata*)data)->za;
triangle_MultyColor_8888_loop2_deff - собирает часть треугольника из этих линий.
>Можно подробнее?
Константа только вектор Z. Это глубина треугольника по вершинам в барицентрических координатах.
+ Показать
− Скрыть
Z.X := (1.0 / V2.Z);
Z.Y := (1.0 / V0.Z);
Z.Z := (1.0 / V1.Z);
Z.W := (0.0);
Вектор точки Pt у меня не константа. Это интерполируемый край. AX+BY+C=0.
Он приращается простым сложением. Pt := Pt + Delta; Получается следующий пиксел.
Если пиксел <= 0, то точка внутри треугольника. (У меня отрицательная площадь).
Потом идет приведение пиксела к барицентрическим координатам в треугольнике, т.е. умножаем на площадь.
Потом вычисляем Z (формула в первом посте) и сравниваем с Z-Buffer'ом.
У меня приложение x64, поэтому double. Он быстрее обрабатывается в x64 чем single.
Кроме того у single глюки. Z-Fighting краев полигона. Видно если прозрачность включить.
С даблом меньше проблем.
>foxes
Это неинтерполируемый треугольник видимо.
Таких алгоритмов у меня куча. Они не правильные.
Интерполяция идет только по формуле: AX+BY+C=0.
Остальное неправильно. Хотя на треугольник похоже.
Получается идеально, но медленно :(
+ Показать
eDmk
Как вариант, если вектора нормализированные и скалярное произведение меньше 1, то можно воспользоваться разложением в ряд тейлора и считать до скажем 4-го члена ряда:
при

eDmk
> Это неинтерполируемый треугольник видимо.
Что значит неинтерполируемый?
foxes
> интерполяция представлена в целых числах как числа с фиксированной точкой. Для
> цвета 8 бит после запятой, для глубины 15 бит после запятой.
eDmk
> Потом вычисляем Z (формула в первом посте) и сравниваем с Z-Buffer'ом.
Для целей Z-буффера деление нафиг не нужно, 1/Z работает так же хорошо, как и Z.
Перспективное деление требуется для корректной интерполяции текстурных координат и прочих атрибутов.
> У меня приложение x64, поэтому double. Он быстрее обрабатывается в x64 чем single.
Полный бред. SSE/AVX позволяют работать с float-ами в два (и больше в случае комплексных команд) раза быстрее.
Растеризаторы ложаться под SIMD идеально, поэтому никаких оправданий использованию double нет.
Собственно, то что GPU используют преимущественно float является подтверждению этого тезиса.
> Кроме того у single глюки. Z-Fighting краев полигона. Видно если прозрачность включить.
Вот поэтому все нормальные растеризаторы работают с целыми числами для границ треугольников.
Заодно отсекаются узкие сверхтонкие треугольники, которые являются проблемой.
}:+()___ [Smile]
>Полный бред. SSE/AVX позволяют работать с float-ами в два (и больше в случае комплексных команд) раза быстрее.
Комплексных? Даже несчастные умножение/деление - скалярные - быстрее (умножение - только по latency).
| Instruction | Latency | Reciprocal throughput |
|---|
| MULSS MULPS | 4 | 1 |
| MULSD MULPD | 5 | 1 |
| DIVSS DIVPS | 7-14 | 7-14 |
| DIVSD DIVPD | 7-22 | 7-22 |
| RCPSS/PS | 3 | 2 |
Это Агнер Фог, для i7.
eDmk
>Константа только вектор Z.
>Вектор точки Pt у меня не константа.
И что?
Как это противоречит тому, что
Pt.X*Z.X+Pt.Y*Z.Y+Pt.Z*Z.Z=S*Z.Z+Pt.X*(Z.X-Z.Z)+Pt.Y*(Z.Y-Z.Z)
где Pt.X+Pt.Y+Pt.Z=S (которое константа что по смыслу барицентрических координат)?
Ну и в любом случае, если
>Константа только вектор Z.
>Он приращается простым сложением. Pt := Pt + Delta;
то InvZ=Pt.X*Z.X+Pt.Y*Z.Y+Pt.Z*Z.Z тоже инкрементируется простым сложением: InvZ:=InvZ+DeltaInvZ, где DeltaInvZ=Delta.X*Z.X+Delta.Y*Z.Y+Delta.Z*Z.Z.
>У меня приложение x64, поэтому double. Он быстрее обрабатывается в x64 чем single.
Очень странное утверждение. Походу неправда. }:+()___ [Smile] дело говорит.
>Кроме того у single глюки. Z-Fighting краев полигона. Видно если прозрачность включить.
Опять же, }:+()___ [Smile] в теме: обычно pixel coverage делают в целых числах с субпиксельной точностью.
FordPerfect
> Комплексных? Даже несчастные умножение/деление - скалярные - быстрее (умножение - только по latency).
Комплексные — это те, которые не имеют специализированного железа и выполняются софтварно микрокодом.
Отличить их можно по плавающей производительности — это деление, корень и все что сложнее.
Умножение же — это чисто хардварная операция (возможно, за исключением NaN-ов и денормалов).
Скорее всего, выполняется на общем железе для float/double (а возможно и для целых), непонятно откуда там дополнительная задержка.
Кстати, хорошо видно, что "скалярные" операции на самом деле векторные с отключенной записью результата.