Необъяснимая разница в результатах qsort () под macOS и Linux - PullRequest
0 голосов
/ 01 мая 2018

Недавно у меня был студент, который написал в компараторе:

int comparator (const void *p, const void *q) {
    const data_t *indexp = *(data_t **)p;
    const data_t *indexq = *(data_t **)q;
    return indexp->index > indexq->index;
}

Это работало без проблем под Ubuntu 16.04 LTS, но не смогло правильно отсортировать при запуске под macOS. Простого изменения > на - было достаточно, чтобы он работал на обеих платформах.

Итак, что здесь происходит? Единственное, о чем я могу думать, это то, что неравенство означает потерю значения -1, но, по-видимому, реализация в Linux (или GCC) qsort() не затронута?

Как это могло быть?

EDIT:

Я хотел проверить, относится ли это к конкретному компилятору, поэтому я скомпилировал с помощью clang под Ubuntu 16.04, и никаких изменений нет, он работает там просто отлично. Итак, я думаю, что есть что-то в фактической реализации qsort() в libc против macOS (BSD libc?), Которая вызывает это. Мне просто очень интересно узнать, что это такое!

Ответы [ 2 ]

0 голосов
/ 01 мая 2018

Нет необходимости указывать на то, что функция сравнения в OP некорректна, и ее использование - неопределенное поведение. Тем не менее, интересно спросить, как это могло бы работать.


Хотя для интерфейса qsort требуется функция компаратора, которая возвращает целое число, обеспечивающее три возможных возвращаемых значения, ничто не обязывает реализацию qsort использовать всю эту информацию. Он всегда может выполнять одну и ту же простую двустороннюю ветвь. Это было бы похоже на функцию sort стандартной библиотеки C ++ , компаратор которой возвращает логическое значение true, если первый аргумент строго меньше второго аргумента.

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

if (*cmp(p, q) < 0) ...

Однако, если компаратор всегда вызывается как:

if (*cmp(p, q) > 0) ...

(или, что эквивалентно, <=), тогда компаратор вашего студента будет работать без проблем.

Поскольку glibc является открытым исходным кодом, это легко проверить, как только мы найдем правильную функцию сортировки. Но не все так, как кажется. Существует исходный файл с именем qsort.c (в каталоге stdlib), который реализует алгоритм быстрой сортировки (, а не алгоритм интросорта, используемый большинством современных реализаций std::sort), и в этом файле мы можем увидеть последовательное использование строгого меньше, чем. Например, из основного внутреннего цикла :

while ((*cmp) ((void *) left_ptr, (void *) mid, arg) < 0)
  left_ptr += size;
while ((*cmp) ((void *) mid, (void *) right_ptr, arg) < 0)
  right_ptr -= size;

(В функции есть много других сравнений, но все они имеют одинаковую форму.)

Так что это не должно работать вообще с компаратором вашего студента. Однако оказывается, что эта функция обычно не вызывается qsort. qsort фактически определено в файле msort.c, который реализует сортировку слиянием. А функция сортировки слиянием выполняет тесты «меньше или равно». Например:

 if ((*cmp) (b1, b2, arg) <= 0)

Реализация сортировки слиянием не на месте, что будет медленнее; это требует временной памяти. И это может быть проблемой, потому что qsort должно работать, даже если временная память не доступна. Таким образом, фактическая реализация qsort начинается с попытки выяснить, достаточно ли физической памяти. (Требуется физическая память, а не пространство подкачки, чтобы избежать подкачки. Также помните, что Linux выполняет оптимистичное распределение, так что тот факт, что malloc завершается успешно, не обязательно означает, что выделенные адреса виртуальной памяти действительно могут использоваться.) Здесь он вычислил необходимое временное пространство (в size) и sysconf, чтобы узнать, сколько физической памяти имеет хост. (Он делит это значение на четыре, чтобы не занимать всю физическую память.)

/* If the memory requirements are too high don't allocate memory.  */
if (size / pagesize > (size_t) phys_pages)
  {
    _quicksort (b, n, s, cmp, arg);
    return;
  }

Таким образом, если сортировка слиянием потребует слишком много памяти, она откладывается до _quicksort, что является именно той функцией, из которой я цитировал ранее, в qsort.c.

В итоге:

  • Неисправная функция компаратора будет работать с реализацией qsort в glibc при условии, что сортируемый массив не слишком велик.

  • Если массив слишком большой, сортировка не удастся.

В отличие от этого, реализация BSD qsort использует все три возвращаемых значения компаратора. В его внутреннем цикле мы видим:

    while (pb <= pc && (cmp_result = CMP(thunk, pb, a)) <= 0) {
        if (cmp_result == 0) {
            swap_cnt = 1;
            swap(pa, pb);
            pa += es;
        }
        pb += es;
    }
    while (pb <= pc && (cmp_result = CMP(thunk, pc, a)) >= 0) {
        if (cmp_result == 0) {
            swap_cnt = 1;
            swap(pc, pd);
            pd -= es;
        }
        pc -= es;
    }

Есть также звонки компаратору, чтобы найти точку опоры; Это все формы <, поэтому они также дадут неправильный ответ. Однако не все потеряно; для очень маленьких векторов (менее семи элементов) вместо этого используется сортировка вставкой, а в цикле сортировки вставки используется более строгое сравнение.

0 голосов
/ 01 мая 2018

Использование > действительно ошибка. Эта операция даст только результат 0 или 1. Использование - является правильным методом.

Причина, по которой это сработало на одном, а не на другом, наиболее вероятна из-за того, что список для сортировки был изначально упорядочен. Если сомневаетесь, запустите код через Valgrind в Linux.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...