openlang

Цикл, который выделял память на каждый столбец

Срезы в Julia копируют. Это известно, но внутри цикла вылетело из головы. Возьмём функцию, которая идёт по столбцам матрицы и для каждого j делает s += sum(A[:, j]). Каждый A[:, j] строит новый вектор, суммирует его и выбрасывает. @time показал, что выделений становится больше вместе с числом столбцов, а в советах по производительности неожиданное выделение памяти названо почти всегда признаком проблемы.

view(A, :, j) или @views перед циклом превращают срез в SubArray, который смотрит прямо в A, и копирование исчезает.

Любопытно стало, что ещё меняет @views. Он превращает в представление каждый срез в обёрнутом выражении, в том числе справа от присваивания. Значит, x = A[:, j] внутри блока @views даёт представление, и запись в x потом пишет в A. Вполне разумно, когда это видишь. Но легко пропустить, если повесить @views на всю функцию ради одного цикла, а где-то ниже функция нарочно копирует столбец.

Разговор продолжается 4

Следуя доводам
  1. Ссылка на ответ

    Первый вызов @time включает компиляцию, поэтому его выделения памяти не относятся к циклу. Читать нужно второй вызов. Глобальная A выделяет память сама по себе, поэтому передавайте её аргументом. Затем сравните две ширины матрицы. Число выделений, растущее вместе с числом столбцов, указывает на цикл.

  2. Ссылка на ответ

    Метод liv_n всё решает, дело в цикле, а не в компиляции. Внутри функции, когда A передаётся аргументом и смотришь только второй вызов, без представлений выделения растут с числом столбцов, а с @views на цикле перестают расти.

    Одно предупреждение для тех, кому захочется поднять @views на всю функцию. Это ровно то, о чём я предупреждал в своём посте.

    @views function colsums!(out, A)
        for j in axes(A, 2)
            out[j] = sum(A[:, j])
        end
        backup = A[:, 1]
        A[:, 1] .= 0
        return backup
    end

    backup должен быть копией. Под @views это представление, так что после строки с обнулением он тоже весь из нулей. Цифры по выделениям выглядят отлично, а функция возвращает не то. Так что @views только на цикл.

  3. Ссылка на ответ

    Между циклом и целой функцией есть промежуточный вариант. @views оставить там, где он стоит, а намеренную копию записать как copy(A[:, 1]). Внутри макроса срез сначала становится представлением, и copy строит из него новый массив, так что backup переживает обнуление. Вне макроса та же строка копирует на один раз больше, чем нужно, и это безвредно. В обоих случаях смысл строки не меняется, если кто-то потом перенесёт макрос, и меня волновало именно это.

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

  4. Ссылка на ответ

    Смотрите не только на число выделений, но и на байты. Если @allocated второго вызова, делённое на число столбцов, близко к одному столбцу Float64, примерно восемь байт на строку, то дело в срезах и больше ни в чём.

Другие стороны того же вопроса

  1. sum(X) на матрице из одной строки

    Функция берёт матрицу наблюдений, строки - наблюдения, столбцы - признаки, и нормирует каждый столбец через X ./ sum(X). Долг…

    Ответы 6matlab
  2. na.rm = TRUE на группе, где всё NA

    Итоги по группам через tapply(x, g, sum, na.rm = TRUE), и одна группа вернулась с 0. Все значения в ней NA. Ни одного измерен…

    Ответы 4r
  3. f2py копирует мой C-массив при каждом вызове

    Передаю массив NumPy в подпрограмму на Фортране через f2py. Массив создан как обычно, np.zeros((n, m)), то есть в C-порядке.…

    Ответы 3fortran