Те, кто хотят использовать в программе предметное деление с остатком сталкиваются с тем, что во многих языках программирования деление сделано по-другому, что обусловлено воплощением в распространённых машинных языках. Если для обычного деления остаток всегда ≥ 0, то во многих Си-подобных языках остаток отрицательный для отрицательного делимого, которое не делится нацело.
делимое | делитель | частное | остаток | / | % |
---|---|---|---|---|---|
5 | 3 | 1 | 2 | 1 | 2 |
-5 | 3 | -2 | 1 | -1 | -2 |
-4 | 3 | -2 | 2 | -1 | -1 |
-3 | 3 | -1 | 0 | -1 | 0 |
Я рассмотрю воплощение получения частного для положительного делителя.
Для положительного делимого всё просто, так как результаты совпадают.
int div(int n, int d) {
assert(n >= 0);
return n / d;
}
Для отрицательного делимого требуется корректировка. Можно догадаться, что если делимое увеличить по модулю на делитель без единицы, то это позволит получить искомый результат.
int div(int n, int d) {
assert(n < 0);
return (n - (d - 1)) / d;
}
Этот вариант плох тем, что допускает переполнение при вычитании из делимого. Его можно преобразовать, вынеся вычитание делителя за пределы деления
int div(int n, int d) {
assert(n < 0);
/* (n + 1 - d) / d */
return (n + 1) / d - 1;
}
Если отрицательное делимое в языке нежелательно, например, из-за применения ISO C90, то нужно преобразовать выражение ещё раз
int div(int n, int d) {
assert(n < 0);
return -1 - (-1 - n) / d;
}
Этот вариант хорош и тем, что для чисел, кодируемых дополнительным кодом вычитание из -1 эквивалентно побитовому отрицанию.
int div(int n, int d) {
assert(n < 0);
return ~((~n) / d);
}
Нет большого смысла писать такой код напрямую в случае использования оптимизирующего компилятора, так как компилятор сам отлично преобразовывает вычитание в более эффективную инструкцию. Но этот код, если его, наконец, свести с делением положительного делимого, подталкивает к следующему изменению.
int div(int n, int d) {
int r;
if (0 <= n) {
r = n / d;
} else {
r = ~((~n) / d);
}
return r;
}
Видно, что разница между ветками для неотрицательного и отрицательного делимого невелика, а, значит, можно попытаться свести их к единому выражению, исключив ветвление.
Известно, что для логического типа отрицание тождественно "исключительному или" с TRUE
NOT a = a XOR TRUEА "исключительное или" с FALSE эквивалентно другому параметру в исходном виде
a = a XOR FALSEВ побитовом выражении для 32-битных чисел, которые можно рассматривать как массивы логических значений, это эквивалентно
~a = a ^ 0xFFFFFFFF
a = a ^ 0x00000000Для сведения выражений достаточно ввести элемент, который равен 0 для неотрицательного делимого и -1 (все биты = 1, то есть 0xFFFFFFFF) для отрицательного. Такое поведение обеспечивается арифметическим побитовым сдвигом вправо на количество бит минус один.
int div(int n, int d) {
int mask;
mask = n >> 31;
return mask ^ ((mask ^ n) / d);
}
Такой код верен для Java и для тех компиляторов Си, которые дают соответствующие гарантии насчёт размера int и представления отрицательных чисел. Как минимум, это работает с gcc, clang, tcc, CompCert. Для общего случая в Си он не подходит из-за стандарта, который отдаёт определение части семантики на откуп создателям компиляторов, а те, зачастую, опираются на поведение целевой платформы.
Для общего случая в Си следует ограничиться промежуточным вариантом, который в сведённом виде может выглядеть так:
int div(int n, int d) {
int r;
if (0 <= n) {
r = n / d;
} else {
r = -1 - ((-1 - n) / d);
}
return r;
}
Схожим образом выводится функция вычисления остатка для Java и некоторых компиляторов C:
int mod(int n, int d) {
int mask;
mask = n >> 31;
return (d & mask) + (mask ^ ((mask ^ n) % d));
}
Для общего случая:
int mod(int n, int d) {
int r;
if (0 <= n) {
r = n % d;
} else {
r = d + (-1 - (-1 - n) % d);
}
return r;
}
Комментариев нет:
Отправить комментарий