Подтвердить что ты не робот

C 64-разрядная производительность цикла на x86

Мне нужна функция контрольной суммы Интернета (одна контрольная сумма) для некоторого кода обработки ICMP ICv4 с использованием сырых сокетов, и я наткнулся на поведение, которое я не могу объяснить на 64-разрядном процессоре Intel (с использованием gcc 4.8.2). Мне было интересно, может ли кто-нибудь пролить свет на него.

Я реализовал первую функцию контрольной суммы, используя 32-разрядный аккумулятор и выполнив 16-разрядные суммы. Затем я реализовал то же самое с использованием 64-разрядного аккумулятора и 32-разрядных сумм, считая, что меньшее количество сумм приведет к более быстрому выполнению. В результате первая реализация выполняется в два раза быстрее, чем вторая (с оптимизацией O3). Я просто не могу понять, почему...

Приведенный ниже код фактически не выполняет точные контрольные суммы (я упростил его), но иллюстрирует проблему. Оба скомпилированы как 64-разрядные, работающие на 64-битной собственной платформе (LP64: короткие 16-битные, int 32-разрядные, длинные 64-разрядные, указатели 64-разрядные).

  • 32-разрядный аккумулятор и 16-разрядные суммы

    unsigned short
    cksum_16_le(unsigned char* data, size_t size)
    {
        unsigned short word;
        unsigned int sum = 0;
        unsigned int i;
    
        for(i = 0; i < size - 1; i += 2)
            sum += *((unsigned short*) (data + i));
    
        sum = (sum & 0xffff) + (sum >> 16);
        sum = (sum & 0xffff) + (sum >> 16);
    
        return ~sum;
    }
    

50 000 вызовов функций по тем же самым данным 10k: ~ 1,1 секунды.

  • 64-разрядный аккумулятор и 32-разрядные суммы

    unsigned short
    cksum_32_le(unsigned char* data, size_t size)
    {
        unsigned long word;
        unsigned long sum = 0;
        unsigned int i;
    
        for(i = 0; i < size - 3; i += 4)
            sum += *((unsigned int*) (data + i));
    
        sum = (sum & 0xffffffff) + (sum >> 32);
        sum = (sum & 0xffffffff) + (sum >> 32);
        sum = (sum & 0xffff) + (sum >> 16);
        sum = (sum & 0xffff) + (sum >> 16);
    
        return ~sum;
    }
    

50 000 вызовов функций по тем же самым данным 10k: ~ 2,2 секунды.

Update:

Кажется, что проблема связана с оборудованием. Запуск диагноза памяти выявил случайные ошибки четности шины (не уверен, почему это повлияет на 32-битную версию больше, чем на 16-разрядную версию, но там вы идете). Код работает как ожидалось на других серверах. Удалит вопрос в течение ближайших нескольких часов (связавшись с оборудованием, он больше не особенно полезен).

Окончательное обновление:

Интересно, что замена циклов for на циклы while и компиляция с оптимизацией O3 (показана ниже для случая с 64-разрядным аккумулятором) приводит к тому, что 32-разрядные и 64-разрядные накопители будут работать с одинаковой скоростью. Это связано с тем, что компилятор выполняет некоторую циклическую развертку (как ни странно, он не разворачивает цикл for) и выполняет суммирование с помощью регистров mmx...

uint64_t sum = 0;
const uint32_t* dptr = (const uint32_t*) data;

while (size > 3)
{
    sum += (uint32_t) *dptr++;
    size -= 4;
}
4b9b3361

Ответ 1

Ранее у меня была аналогичная проблема; Я не мог найти никаких проблем в любом из наших кодов. НО что сработало для меня, это изменение компилятора.

Я предполагаю, что GCC пишет устаревшую сборку.

Если вы можете декомпилировать свое приложение, мы можем пролить свет на проблему, но здесь недостаточно информации, чтобы продолжить здесь.

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

Надеюсь, это помогло вам, информации об этом практически нет.

Если бы я должен был предположить, что согласен с Learner, я вполне уверен, что декомпилированный код будет указывать на цикл for. Я очень заинтересован в этом вопросе, поэтому прокомментируйте это.

Ответ 2

Вероятный ответ: условие "i < size-1" может быть скомпилировано и выполнено более эффективно, чем "i < size-3". Сначала требуется только инструкция декремента вместо другой, которая требует, чтобы константа 3 также была загружена. Этот расчет выполняется с каждой итерацией. Вы должны сохранить результат этого расчета в другом месте.

Это не имеет никакого отношения к циклу while. Когда вы переписываете цикл while, вы также изменили условие итерации и устранили причину выше.

Я также предпочел бы делать кастинг типа вне цикла, но это также показывает одно ограничение - ваши данные должны

Ответ 3

Вы затрудняете работу компилятора. Во внутреннем цикле вы вычисляете смещение байта самостоятельно по вашему выбору шага индекса и приведения. Это может препятствовать разворачиванию цикла или любой другой оптимизации, которая пытается выполнить выравнивание. Нельзя также позволить компилятору использовать режимы адресации и выходить, чтобы вычислить сам эффективный адрес (или LEA it).

Если бы я делал это, я бы бросил указатель данных в верхней части цикла на ваш тип шага и увеличил счетчик циклов на 1. Компилятор может быть немного счастливее.

Ответ 4

Я думаю, что он не может развернуть цикл "for" из-за приведения из char * в unsigned int *. Приведение типов часто не позволяет компилятору оптимизировать код, потому что в этом случае невозможно провести идеальный анализ псевдонимов. Если вы сначала объявите дополнительный локальный указатель, чтобы передать свой "указатель данных" перед циклом, чтобы в цикле не было никакого перевода, компилятор должен иметь возможность оптимизировать цикл "для".

Ответ 5

sum += *((unsigned int*) (data + i)); 

Мне не нравится такой актер в

64-разрядный аккумулятор и 32-разрядные суммы

так как вы написали:

Оба скомпилированы как 64-разрядные, запущенные на 64-битной собственной платформе (LP64: короткие 16-битные, int 32-разрядные, длинные > 64-разрядные, указатели 64-разрядные).

Я бы предложил использовать (unsigned long *). Некоторые люди советуют проверять в разобранном коде, что происходит в действительности. Я думаю, это из-за вашего int * cast с длинным аккумулятором.

Как насчет без флага O2 < > O3? Не могли бы вы показать, что такое скорость в обычном режиме компиляции?