2016-09-22 9 views
5

Я написал простую программу, которая выполняет кучу инструкций NOP в цикле, и, к моему удивлению, она выполняет около 10600000000 из них в секунду или около 10 ГГц, тогда как мой процессор составляет всего 2,2 ГГц.Может ли мой процессор выполнять несколько NOP за каждый цикл процессора?

Как это возможно? Является ли процессор рассматривающим их как один мега-NOP, или я просто обнаружил, что означает «параллелизм уровня инструкций»?

Что было бы лучше для инструкций в секунду? Выполнение добавления инструкций достигает только 414900000/s, десятая часть BogoMips сообщает мой CPU: 4390,03

код C:

#include <stdio.h> 
#include <stdint.h> 
#include <time.h> 

#define ten(a) a a a a a a a a a a 
#define hundred(a) ten(a) ten(a) ten(a) ten(a) ten(a) ten(a) ten(a) \ 
     ten(a) ten(a) ten(a) 

#define ITER 10000000 
int main(void) { 
    uint64_t i=0; 
    uint64_t t=time(NULL); 
    while(1) { 
    for(int j=0; j<ITER;j++) { 
    hundred(asm volatile ("nop");) 
    } 
    i+=ITER*100; 
    printf("%lu/%lu\n", i, time(NULL)-t); 
    } 
    return 0; 
} 

Составитель сборки:

.file "gbloopinc.c" 
    .section .rodata 
.LC0: 
    .string "%lu/%lu\n" 
    .text 
    .globl main 
    .type main, @function 
main: 
.LFB0: 
    .cfi_startproc 
    pushq %rbp 
    .cfi_def_cfa_offset 16 
    .cfi_offset 6, -16 
    movq %rsp, %rbp 
    .cfi_def_cfa_register 6 
    subq $32, %rsp 
    movq $0, -16(%rbp) 
    movl $0, %edi 
    call time 
    movq %rax, -8(%rbp) 
.L4: 
    movl $0, -20(%rbp) 
    jmp .L2 
.L3: 
#APP 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
# 15 "gbloopinc.c" 1 
    nop 
# 0 "" 2 
#NO_APP 
    addl $1, -20(%rbp) 
.L2: 
    cmpl $9999999, -20(%rbp) 
    jle .L3 
    addq $1000000000, -16(%rbp) 
    movl $0, %edi 
    call time 
    subq -8(%rbp), %rax 
    movq %rax, %rdx 
    movq -16(%rbp), %rax 
    movq %rax, %rsi 
    movl $.LC0, %edi 
    movl $0, %eax 
    call printf 
    jmp .L4 
    .cfi_endproc 
.LFE0: 
    .size main, .-main 
    .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.2) 5.4.0 20160609" 
    .section .note.GNU-stack,"",@progbits 
+0

Сколько ядер (физических и логических)? –

+0

Что вы подразумеваете под «лучшей мерой для инструкций в секунду»? Различные инструкции занимают разные промежутки времени, есть параллелизм, спекулятивное выполнение, конвейерные киоски, отклонение от ветви (неверное), промахи в кеше и т. Д. Что вы на самом деле пытаетесь измерить? – Art

+0

4 ядра с гиперпотоком. Я пытаюсь измерить влияние размера кода на скорость, в значительной степени. Более большой код не подходит для кеша, поэтому я ожидаю, что он будет медленнее. – Pepijn

ответ

5

Это не имеет никакого отношения к нескольким ядрам. Ядра не являются «портами».


4 NOPS за такт ширина вопрос/выбытие трубопровода вашего суперскалярного/испорченный CPU. Для NOP даже не требуется блок исполнения/выполнения (ALU или загрузка или сохранение), поэтому вы даже не ограничены количеством целых исполняемых блоков. Даже Core2 (первый четырехъядерный процессор Intel x86) может запускать 4 NOP за такт.

Как вы уже догадались, это пример Параллелизма на уровне инструкций. Конечно, NOP не имеют входных зависимостей.

На вашем процессоре Sandybridge (с тремя исполнительными блоками ALU на ядро) вы можете запустить 3 ADD и одну команду загрузки или хранения на такт, так как ширина его конвейера составляет 4 раза. См. Agner Fog's microarch pdf и другие ссылки в tag wiki. На потоке независимых инструкций ADD, как

add eax, eax 
add ebx, ebx 
add ecx, ecx 
add edx, edx 
... 

вы бы увидели около 3 часов в пропускной способности на SnB, узкие места на порты выполнения целого ALU. Haswell может запустить это с 4 ADD за такт, потому что у него есть четвертый порт выполнения ALU, который может обрабатывать не-векторные целочисленные операторы (и ветви).

Процессоры нестандартного типа, как правило, имеют более широкую ширину интерфейса и длину выхода/выхода, чем количество исполнительных блоков. Имея больше инструкций, декодированных и готовых к выполнению, как только свободное исполнительное устройство увеличивает их использование. В противном случае механизм внешнего порядка мог видеть только то, что в настоящее время выполняется, если выполнение остановилось или замедлилось из-за последовательных зависимостей. (например.add eax,eax/add eax,eax нуждается в выходе первого добавления в качестве ввода ко второму добавлению, поэтому он может работать только с одним insn за такт.)

+0

Интересно, поэтому, если я делаю цикл, который увеличивает 4 счетчика и затем суммирует их в конце, цикл будет работать в 4 раза быстрее, чем простой цикл. Интересно, может ли оптимизационный компилятор использовать это. – Pepijn

+0

@Pepijn: да, несколько аккумуляторов - известная оптимизация. Это наиболее полезно для с плавающей запятой, где даже добавление FP имеет более 1 цикла латентности.К сожалению, большинство компиляторов (gcc и clang как минимум) обычно не делают этого даже при разворачивании. Я думаю, что clang иногда использует пару аккумуляторов при разворачивании по умолчанию 4 раза. Если вы используете FMA на Intel Haswell, задержка = 5 и пропускная способность = один на 0,5 такта, вам нужно 10 аккумуляторов, чтобы поддерживать 10 FMA в полете и насыщать эти исполнительные блоки. –

4

Я расширить больше Комментарий Хансапасса.

Современные процессоры являются суперскалярными и многоядерными. Легко понять, что такое многоядерный процессор - он имеет несколько ядер. С другой стороны, Superscalar требует немного больше знаний об оборудовании. This is a stackexchange question объясняет, что означает, что процессор должен быть суперскалярным. Суперскалярные процессоры имеют много функциональных блоков в одном ядре и сильно конвейерны. Вот почему несколько инструкций могут быть отправлены одновременно и в одном ядре. Вот некоторые из функциональных единиц в процессоре: целочисленное сложение/вычитание, умножение с плавающей запятой, деление с плавающей запятой, целочисленное умножение, целочисленное деление.

Предлагаю вам больше узнать о суперскалярных процессорах и узнать больше о вашем процессоре в частности.

+1

NOP даже не требует целочисленного исполнения. CPU OP имеет ширину ширины и ширины 4-х, но только три порта выполнения ALU. Кроме того, каждый порт ALU может обрабатывать целые команды добавления. Целочисленное умножение реже и требует гораздо больше транзисторов для реализации, поэтому Intel и AMD только поместили один скалярный целочисленный блок умножения на каждое ядро ​​(на один порт выполнения). Тем не менее, ваш ответ - полезное упрощение и должен был установить процесс мышления OP на правильном пути, но я просто подумал, что хочу указать, что он еще сложнее, чем вы описываете. –

+0

@PeterCordes Спасибо за дополнительную информацию, особенно о процессоре OP. Я знаю, что это намного сложнее, чем я описал - у меня есть целый учебник по компьютерной архитектуре/системам, но я попытался обобщить и упростить. Мне действительно нужно отредактировать свой пост, чтобы исправить мою грамматику, хотя это ужасно. – mgarey

+0

@PeterCordes: Конечно, если мы попадаем в сорняки, если целые числа умножаются на константы, то компилятор может преобразовать умножения в некоторую комбинацию команд сдвигов, добавлений и/или адресной нагрузки (lea) , поэтому, даже если вы используете целочисленные умножения, вы можете найти свой код, превышающий теоретические скорости, ожидаемые только от одного целочисленного порта с несколькими значениями. – ShadowRanger

 Смежные вопросы

  • Нет связанных вопросов^_^