Использование size_t для длины влияет на оптимизацию компилятора? - PullRequest
0 голосов
/ 17 января 2019

Читая этот вопрос , я увидел первый комментарий, в котором говорилось:

size_t для длины не очень хорошая идея, для оптимизации / UB подходящие типы имеют подпись.

с последующим комментарием в поддержку рассуждений. Это правда?

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

Ответы [ 3 ]

0 голосов
/ 17 января 2019

size_t отсутствие подписи - это в основном историческая случайность - если ваш мир 16-битный, переход от 32767 к 65535 максимальному размеру объекта является большой победой; в современных вычислительных системах (где 64 и 32-битные являются нормой) тот факт, что size_t без знака, в основном неприятен.

Хотя неподписанные типы имеют меньше неопределенное поведение (поскольку гарантируется перенос), тот факт, что они имеют в основном семантику "битового поля", часто является причиной ошибок и других неприятных сюрпризов; в частности:

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

    unsigned a = 10, b = 20;
    // prints UINT_MAX-10, i.e. 4294967286 if unsigned is 32 bit
    std::cout << a-b << "\n"; 
    
  • в общем, в знаковых / беззнаковых сравнениях и математических операциях выигрыши без знака (поэтому значение со знаком преобразуется в беззнаковое неявное значение), что, опять же, приводит к неожиданностям;

    unsigned a = 10;
    int b = -2;
    if(a < b) std::cout<<"a < b\n"; // prints "a < b"
    
  • в обычных ситуациях (например, итерация в обратном направлении) семантика без знака часто проблематична, так как вы хотите, чтобы индекс стал отрицательным для граничного условия

    // This works fine if T is signed, loops forever if T is unsigned
    for(T idx = c.size() - 1; idx >= 0; idx--) {
        // ...
    }
    

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

int sum_arr(int *arr, unsigned len) {
    int ret = 0;
    for(unsigned i = 0; i < len; ++i) {
        ret += arr[i];
    }
    return ret;
}

// compiles successfully and overflows the array; it len was signed,
// it would just return 0
sum_arr(some_array, -10);

Для части оптимизации: преимущества подписанных типов в этом отношении переоценены; да, компилятор может предположить, что переполнение никогда не произойдет, поэтому он может быть очень умным в некоторых ситуациях, но, как правило, это не изменит игру (так как в общем случае семантика циклического перехода «бесплатна» в современных архитектурах); самое главное, как обычно, если ваш профилировщик обнаружит, что определенная зона является узким местом, вы можете изменить ее, чтобы она работала быстрее (включая переключение типов локально, чтобы компилятор генерировал лучший код, если вы считаете это выгодным).

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

0 голосов
/ 18 января 2019

Я поддерживаю мой комментарий.

Есть простой способ проверить это: проверить, что генерирует компилятор.

void test1(double* data, size_t size)
{
    for(size_t i = 0; i < size; i += 4)
    {
        data[i] = 0;
        data[i+1] = 1;
        data[i+2] = 2;
        data[i+3] = 3;
    }
}

void test2(double* data, int size)
{
    for(int i = 0; i < size; i += 4)
    {
        data[i] = 0;
        data[i+1] = 1;
        data[i+2] = 2;
        data[i+3] = 3;
    }
}

Так что же генерирует компилятор? Я ожидал бы разворачивания цикла, SIMD ... для чего-то такого простого:

Давайте проверим крестник.

Ну, подписанная версия имеет развёртывание, SIMD, а не беззнаковую.

Я не собираюсь показывать какие-либо тесты, потому что в этом примере узкое место будет связано с доступом к памяти, а не с вычислениями ЦП. Но вы поняли.

Второй пример, просто оставьте первое назначение:

void test1(double* data, size_t size)
{
    for(size_t i = 0; i < size; i += 4)
    {
        data[i] = 0;
    }
}

void test2(double* data, int size)
{
    for(int i = 0; i < size; i += 4)
    {
        data[i] = 0;
    }
}

Как вы хотите gcc

ОК, не так впечатляюще, как для clang, но он все равно генерирует другой код.

0 голосов
/ 17 января 2019

Этот комментарий просто неверен. При работе с собственными операндами размером с указатель на любой разумной архитектуре на уровне компьютеров нет разницы между смещениями со знаком и без знака , и, следовательно, нет места для них, чтобы иметь различные свойства производительности.

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

В конечном итоге вы должны изначально всегда использовать тип, который имеет смысл для вас семантически , а не пытаться выбирать тип для свойств производительности. Вы можете подумать об их изменении только в том случае, если существует серьезная проблема с измеряемой производительностью, которая может быть улучшена за счет компромиссов, связанных с выбором типов.

...