Разница в том, что list.reverse
как функция list
имеет доступ к базовому массиву указателей. Таким образом, он действительно может переставлять указатели, не смотря ни на какие объекты ( source ):
reverse_slice(PyObject **lo, PyObject **hi)
{
assert(lo && hi);
--hi;
while (lo < hi) {
PyObject *t = *lo;
*lo = *hi;
*hi = t;
++lo;
--hi;
}
}
Функции random.shuffle
и numpy.random.shuffle
, с другой стороны, имеют только внешний вид и go через интерфейс списка, который включает кратковременную загрузку объектов для их замены:
random.shuffle
:
def shuffle(self, x, random=None):
...
for i in reversed(range(1, len(x))):
# pick an element in x[:i+1] with which to exchange x[i]
j = randbelow(i+1)
x[i], x[j] = x[j], x[i]
numpy.random.shuffle
:
def shuffle(self, object x, axis=0):
...
for i in reversed(range(1, n)):
j = random_interval(&self._bitgen, i)
x[i], x[j] = x[j], x[i]
Таким образом, существует как минимум потенциал для большого количества ошибок кэша. Но давайте в качестве теста попробуем reverse
in Python:
def my_reverse(x):
lo = 0
hi = len(x) - 1
while lo < hi:
x[lo], x[hi] = x[hi], x[lo]
lo += 1
hi -= 1
Сравнительный тест:
Реверс list(range(n))
был так же быстр, как задний ход [0] * n
, несмотря на загрузку объектов. Причина в том, что Python создает объекты в значительной степени последовательно в памяти. Вот тест с миллионами объектов. Почти все были расположены на 16 байт после предыдущего:
>>> mylist = list(range(10**6))
>>> from collections import Counter
>>> ctr = Counter(id(b) - id(a) for a, b in zip(mylist, mylist[1:]))
>>> for distance, how_often in ctr.most_common():
print(distance, how_often)
16 996056
48 3933
-1584548240 1
-3024 1
2416 1
-2240 1
2832 1
-304 1
-96 1
-45005904 1
6160432 1
38862896 1
Так что неудивительно, что это быстро, поскольку это очень удобно для кэша.
Но теперь давайте используем наш Python реверс на перетасованный список (как в вопросе с list.reverse
, где это не имеет значения):
Большой Разница в том, что my_reverse
загружает объекты случайным образом повсюду, что противоположно кеш-памяти.
И, конечно, это то же самое, что происходит с функциями shuffle
. В то время как list(range(n))
изначально является кеш-памятью, перестановка выбирает случайные индексы j
для обмена, что очень кеш-кеш. И хотя i
просто движется последовательно, он столкнется с множеством уже случайным образом поменянных объектов, так что это также недружественно для кэша.