На 64-битной машине можно ли безопасно работать с отдельными байтами 64-битного квадратного слова параллельно?

Фон

Я делаю параллельные операции над строками и столбцами в изображениях. Мои изображения - 8-битные или 16-битные пиксели, и я на 64-битной машине.  Когда я выполняю параллельные операции с столбцами, два соседних столбца могут использовать один и тот же 32-битный int или 64 бит long. В принципе, я хочу знать, могу ли я безопасно работать с отдельными байтами одного и того же квадрата. Параллельно.

Минимальный тест

Я написал минимальную тестовую функцию, которую я не смог выполнить. Для каждого байта в 64 бит long я одновременно выполняю последовательные умножения в конечном поле порядка p. Я знаю, что небольшая теорема Ферма a^(p-1) = 1 mod p, когда p является простым. Я изменяю значения a и p для каждого из моих 8 потоков, и выполняю k*(p-1) умножения a. Когда потоки заканчивают каждый байт, должно быть 1. И фактически, мои тестовые примеры проходят. Каждый раз, когда я запускаю, я получаю следующий вывод:

8
101010101010101
101010101010101

Моя система Linux 4.13.0-041300-generic x86_64 с 8-ядерным процессором Intel (R) Core i7-7700HQ с частотой 2.80 ГГц. Я скомпилировал с g++ 7.2.0 -O2 и рассмотрел сборку. Я добавил сборку для "INNER LOOP" и прокомментировал это. Мне кажется, что генерируемый код безопасен, потому что магазины записывают только младшие 8 бит в пункт назначения вместо того, чтобы выполнять некоторую поразрядную арифметику и сохраняя все слово или квадрат. g++ -O3 сгенерировал аналогичный код.

Вопрос:

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

#include <iostream>
#include <pthread.h>

class FermatLTParams
{
public:
    FermatLTParams(unsigned char *_dst, unsigned int _p, unsigned int _a, unsigned int _k)
        : dst(_dst), p(_p), a(_a), k(_k) {}

    unsigned char *dst;
    unsigned int p, a, k;
};

void *PerformFermatLT(void *_p)
{  
    unsigned int j, i;
    FermatLTParams *p = reinterpret_cast<FermatLTParams *>(_p);
    for(j=0; j < p->k; ++j)
    {    
        //a^(p-1) == 1 mod p

        //...BEGIN INNER LOOP
        for(i=1; i < p->p; ++i)
        {
            p->dst[0] = (unsigned char)(p->dst[0]*p->a % p->p);
        }
        //...END INNER LOOP

        /* gcc 7.2.0 -O2  (INNER LOOP)

        .L4:
            movq    (%rdi), %r8             # r8 = dst
            xorl    %edx, %edx              # edx = 0
            addl    $1, %esi                # ++i
            movzbl  (%r8), %eax             # eax (lower 8 bits) = dst[0]
            imull   12(%rdi), %eax          # eax =  a * eax
            divl    %ecx                    # eax = eax / ecx;   edx = eax % ecx    
            movb    %dl, (%r8)              # dst[0] = edx (lower 8 bits)
            movl    8(%rdi), %ecx           # ecx = p
            cmpl    %esi, %ecx              # if (i < p)
            ja      .L4                     #   goto L4
        */

    }
    return NULL;
}

int main(int argc, const char **argv)
{
    int i;
    unsigned long val = 0x0101010101010101; //a^0 = 1
    unsigned int k = 10000000;
    std::cout << sizeof(val) << std::endl;
    std::cout << std::hex << val << std::endl;
    unsigned char *dst = reinterpret_cast<unsigned char *>(&val);
    pthread_t threads[8];
    FermatLTParams params[8] = 
    { 
        FermatLTParams(dst+0, 11, 5, k),
        FermatLTParams(dst+1, 17, 8, k),
        FermatLTParams(dst+2, 43, 3, k),
        FermatLTParams(dst+3, 31, 4, k),
        FermatLTParams(dst+4, 13, 3, k),
        FermatLTParams(dst+5, 7, 2, k),
        FermatLTParams(dst+6, 11, 10, k),
        FermatLTParams(dst+7, 13, 11, k)
    };

    for(i=0; i < 8; ++i)
    {
        pthread_create(threads+i, NULL, PerformFermatLT, params+i);
    }
    for(i=0; i < 8; ++i)
    {
        pthread_join(threads[i], NULL);
    }

    std::cout << std::hex << val << std::endl;
    return 0;
}

Ответ 1

Ответ: ДА, вы можете безопасно работать с отдельными байтами 64-битного квадлового дерева параллельно, разными потоками.

Удивительно, что он работает, но это было бы катастрофой, если бы этого не произошло. Все аппаратные средства действуют так, как если бы ядро ​​записывало байт в своем собственном ядре, а не только, что строка кэша грязна, но какие байты внутри нее. Когда эта строка кэша (64 или 128 или даже 256 байт) в конечном итоге записывается в основную память, только грязные байты фактически изменяют основную память. Это важно, поскольку в противном случае, когда два потока работали над независимыми данными, которые занимали одну и ту же строку кэша, они могли бы удалить друг друга.

Это может быть плохо для производительности, потому что способ его работы частично объясняется магией "согласованности кеша", где, когда один поток записывает байт, все кэши в системе, которые имеют ту же самую строку данных, подвержены влиянию. Если они грязные, они должны записываться в основную память, а затем либо бросать строку кэша, либо записывать изменения из другого потока. Существуют разные виды реализации, но обычно это дорого.