Одним тёплым холодным зимним вечером, хотелось согреться в офисе и проверить теорию одного коллеги, что C++ vector мог бы быстрее справиться с задачей, чем CPython list.


В компании мы разрабатываем продукты на базе Django и случилось так, что нужно было обработать один большой массив словарей. Коллега предположил, что реализация на C++ была бы гораздо быстрее, а меня не покидало чувство, что Гвидо и сообщество наверное немного круче нас в Си и возможно уже решили и обошли все подводные камни, реализовав всё гораздо быстрее.


Для проверки теории, я решил написать небольшой тестовый файл, в котором решил прогнать в цикле вставку 1М словарей одинакового содержания в массив и в vector 100 раз подряд.


Результаты хоть и были ожидаемые, но так же и внезапные.


Так уж вышло, что мы активно используем Cython, поэтому в целом результаты будут отличаться на полностью CPython реализации.


Стенд


  • Calculate Linux onegreyonewhite 4.18.14-calculate #1 SMP PREEMPT Sat Oct 13 21:03:27 UTC 2018 x86_64 Intel® Core(TM) i7-4770 CPU @ 3.40GHz GenuineIntel GNU/Linux
  • Python 2.7 и 3.6
  • Cython 0.28.3
  • gcc (Gentoo 7.3.0-r3 p1.4)

Скрипт


К слову, здесь пришлось повозиться. Чтобы получить максимально реальные числа (т.е. не просто сделать супероптимизировано, но и так, что мы потом сможем это использовать без танцев с бубном), пришлось всё делать основном скрипте, а все дополнительные .h свести к минимуму.


Первая проблема заключалась в том, что обёртка Cython для vector не хочет работать в таком виде:


# Так не хотел
ctypedef vector[object] dict_vec
# И так не завелось (ошибка появлялась на vector.push_back(dict()))
ctypedef vector[PyObject*] dict_vec
# И даже так, что удивительно (просто говорит, что не может object привести к PyObject.)
ctypedef vector[PyObject] dict_vec

При всём при этом получали ошибку, что невозможно привести dict к PyObject. Конечно же это проблемы Cython, но так как мы его используем, нам нужно решить эту конкретную проблему.
Пришлось сделать маленький костылик в виде

#include "Python.h"

static PyObject * convert_to_pyobject(PyObject *obj)
{
    return obj;
}

Самое удивительное, что это заработало. Больше всего меня пугает, что я до конца не понимаю почему и какие последствия влечёт.


Итоговые исходники

cython_experiments.h


#include "Python.h"

static PyObject * convert_to_pyobject(PyObject *obj)
{
    return obj;
}

cython_experiments.pyx


# -*- coding: utf-8 -*-
# distutils: language = c++
# distutils: include=['./']
# distutils: extra_compile_args=["-O1"]
from __future__ import unicode_literals
import time
from libc.stdlib cimport free
from cpython.dict cimport PyDict_New, PyDict_SetItemString
from cpython.ref cimport PyObject
from libcpp.string cimport string
from libcpp.vector cimport vector

cdef extern from "cython_experiments.h":
    PyObject* convert_to_pyobject(object obj)

ctypedef vector[PyObject*] dict_vec

range_attempts = 10 ** 6

# Insert time
cdef test_list():
    t_start = time.time()

    data_list = list()
    for i from 0 <= i < range_attempts:
        data_list.append(dict(
            name = 'test_{}'.format(i),
            test_data = i,
            test_data2 = str(i),
            test_data3 = range(10),
        ))

    del data_list

    return time.time() - t_start

cdef test_vector():
    t_start = time.time()

    cdef dict_vec *data_list
    data_list = new dict_vec()
    data_list.resize(range_attempts)

    for i from 0 <= i < range_attempts:
        data = PyDict_New()
        PyDict_SetItemString(data, 'name', 'test_{}'.format(i))
        PyDict_SetItemString(data, 'test_data', i)
        PyDict_SetItemString(data, 'test_data2', str(i))
        PyDict_SetItemString(data, 'test_data3', range(10))
        data_list.push_back(convert_to_pyobject(data))

    free(data_list)

    return time.time() - t_start

# Get statistic

times = dict(list=[], vector=[])
attempts = 100
for i from 0 <= i < attempts:
    times['list'].append(test_list())
    times['vector'].append(test_vector())
    print('''
    Attempt: {}
    List time: {}
    Vector time: {}
    '''.format(i, times['list'][-1], times['vector'][-1]))

avg_list = sum(times['list']) / attempts
avg_vector = sum(times['vector']) / attempts

print('''
Statistics:
attempts: {}
list avg time: {} 
vector avg time: {} 
'''.format(attempts, avg_list, avg_vector))

Попытка 1


Очень хочется, чтобы можно было собирать *.whl для проекта и чтобы это всё завелось на практически любой системе, поэтому сперва был выставлен флаг оптимизации в 0. Это привело к странному результату:


Python 2.7

Statistics:
attempts: 100
list avg time: 2.61709237576
vector avg time: 2.92562381506

Немного поразмыслив, решил что мы всё равно используем флаг -O1, поэтому выставил всё же его и получил:


Python 2.7

Statistics:
attempts: 100
list avg time: 2.49274396896
vector avg time: 0.922211170197

Как-то немного взгруснулось: всё же вера в профессионализм Гвидо и Ко меня подвела. Но потом, я заметил что как-то подозрительно жрёт память скрипт и к концу он подъедал примерно 20Гб ОЗУ. Проблема была в следующем: в итоговом скрипте, можно наблюдать функцию free, после прохода цикла. На этой итерации его ещё не было. Тогда я подумал...


А не отключить ли мне gc?


Между попытками я сделал gc.disable() и после попытки gc.enable(). Запускаю сборку и скрипт и получаю:


Python 2.7

Statistics:
attempts: 100
list avg time: 1.00309731514
vector avg time: 0.941153049469

В целом, разница не большая, поэтому я подумал, что нет смысла переплачивать стараться как-то извратиться и просто использовать CPython, но собирать его по прежнему Cython'ом.
Наверное у многих возник вопрос: "А что там с памятью?" Самое удивительное (нет), что ничего. Она росла с такой же скоростью и в таком же количестве. На ум пришла статья, но лезть в исходники Python совсем не хотелось. Да и означало это лишь одно — проблема в реализации вектора.


Финал


После долгих мучений с приведением типов, а именно, чтобы вектор принимал в себя pointer на словарь, был получен тот самый итоговый скрипт и с включённым gc я получал в среднем разницу в 2.6 раза (вектор быстрее) и относительно хорошую работу с памятью.


Вдруг до меня дошло, что я собираю всё только под Py2.7 и даже не попробовал сделать что-либо с 3.6.


И вот тут я реально удивился (после предыдущих результатов, удивление было закономерным):


Python 3.6

Statistics:
attempts: 100
list avg time: 0.8771139788627624
vector avg time: 1.075702157020569

Python 2.7

Statistics:
attempts: 100
list avg time: 2.61709237576
vector avg time: 0.92562381506

При всём при этом, gc по прежнему работал, память не отжиралась и это был один и тот же скрипт. Понимая, что через уже чуть больше года, нужно будет распрощаться с 2.7, мне всё равно не давало покоя, что между ними такая разница. Чаще всего, я слышал/читал/экспериментировал и Py3.6 был медленнее Py2.7. Однако ребята из Cython-разработчиков сделали что-то невероятное и поменяли ситуацию на корню.


Итог


После этого эксперимента, мы решили сильно не заморачиваться с поддержкой Python 2.7 и переделкой каких-либо частей приложений на C++, просто потому что оно того не стоит. Всё уже написали до нас, нам остаётся это лишь правильно применить для решения конкретной задачи.


UPD 24.12.2018:
По совету iCpu и после выпадов в сторону, что проверяется непонять что и как, постарался переписать C++ часть максимально удобным для разработки в дальнейшем способом, а так же минимизировать абстракции. Получилось ещё хуже:


Результат плохого знания C++

cython_experiments.h


#include "Python.h"
#include <vector>
#include <algorithm>

#ifndef PyString_AsString
#define PyString_AsString PyUnicode_AsUTF8
#define PyString_FromString PyUnicode_FromString
#endif

typedef struct {
    char* name;
    bool reverse;
} sortFiled;

class cmpclass {
    public:
        cmpclass(std::vector<char*> fields) {
            for (std::vector<char*>::iterator it = fields.begin() ; it < fields.end(); it++){
                bool is_reverse = false;
                char* name;
                if (it[0] == "-"){
                    is_reverse = true;
                    for(int i=1; i<strlen(*it); ++i)
                        name[i] = *it[i];
                }
                else {
                    name = *it;
                }
                sortFiled field = {name, is_reverse};
                this->fields_to_cmp.push_back(field);
            }
        }
        ~cmpclass() {
            this->fields_to_cmp.clear();
            this->fields_to_cmp.shrink_to_fit();
        }
        bool operator() (PyObject* left, PyObject* right) {
            //
            bool result = false;
            for (std::vector<sortFiled>::iterator it = this->fields_to_cmp.begin() ; it < this->fields_to_cmp.end(); it++){
                //
                PyObject* str_name = PyString_FromString(it->name);
                PyObject* right_value = PyDict_GetItem(right, str_name);
                PyObject* left_value = PyDict_GetItem(left, str_name);
                if(!it->reverse){
                    result = left_value < right_value;
                } else {
                    result = (left_value > right_value);
                }
                PyObject_Free(str_name);
                if(!result)
                    return false;
            }
            return true;
        }
    private:
        std::vector<sortFiled> fields_to_cmp;
};

void vector_multikeysort(std::vector<PyObject *> items, PyObject* columns, bool reverse)
{
    std::vector<char *> _columns;
    for (int i=0; i<PyList_GET_SIZE(columns); ++i) {
        PyObject* item = PyList_GetItem(columns, i);
        char* item_str = PyString_AsString(item);
        _columns.push_back(item_str);
    }
    cmpclass cmp_obj(_columns);
    std::sort(items.begin(), items.end(), cmp_obj);
    if(reverse)
        std::reverse(items.begin(), items.end());
}

std::vector<PyObject *> _test_vector(PyObject* store_data_list, PyObject* columns, bool reverse = false)
{
    int range_attempts = PyList_GET_SIZE(store_data_list);
    std::vector<PyObject *> data_list;
    for (int i=0; i<range_attempts; ++i) {
        data_list.push_back(PyList_GetItem(store_data_list, i));
    }
    vector_multikeysort(data_list, columns, reverse);
    return data_list;
}

cython_experiments.pyx


# -*- coding: utf-8 -*-
# distutils: language = c++
# distutils: include=['./']
# distutils: extra_compile_args=["-O2", "-ftree-vectorize"]
from __future__ import unicode_literals
import time
from libc.stdlib cimport free
from cpython.dict cimport PyDict_New, PyDict_SetItemString
from cpython.ref cimport PyObject
from libcpp.string cimport string
from libcpp.vector cimport vector
import gc

cdef extern from "cython_experiments.h":
    vector[PyObject*] _test_vector(object store_data_list, object columns, int reverse)

range_attempts = 10 ** 6
store_data_list = list()
for i from 0 <= i < range_attempts:
    store_data_list.append(dict(
        name = 'test_{}'.format(i),
        test_data = i,
        test_data2 = str(i),
        test_data3 = range(10),
    ))

# Insert time
def multikeysort(items, columns, reverse=False):
    items = list(items)
    columns = list(columns)
    columns.reverse()

    for column in columns:
        # pylint: disable=cell-var-from-loop
        is_reverse = column.startswith('-')
        if is_reverse:
            column = column[1:]
        items.sort(key=lambda row: row[column], reverse=is_reverse)

    if reverse:
        items.reverse()

    return items

cdef test_list():
    t_start = time.time()

    data_list = list()
    for i in store_data_list:
        data_list.append(i)

    data_list = multikeysort(data_list, ('name', '-test_data'), True)

    for i in data_list:
        i

    del data_list

    return time.time() - t_start

cdef test_vector():
    t_start = time.time()
    data_list = _test_vector(store_data_list, ['name', '-test_data'], 1)

    for i in data_list:
        i

    return time.time() - t_start

# Get statistic
times = dict(list=[], vector=[])
attempts = 10
gc.disable()
for i from 0 <= i < attempts:
    times['list'].append(test_list())
    times['vector'].append(test_vector())
    gc.collect()
    print('''
    Attempt: {}
    List time: {}
    Vector time: {}
    '''.format(i, times['list'][-1], times['vector'][-1]))

del store_data_list
avg_list = sum(times['list']) / attempts
avg_vector = sum(times['vector']) / attempts

print('''
Statistics:
attempts: {}
list avg time: {} 
vector avg time: {} 
'''.format(attempts, avg_list, avg_vector))

Python 3.6

Statistics:
attempts: 10
list avg time: 0.2640914678573608 
vector avg time: 2.5774293661117555 

Есть идеи, что можно было бы улучшить в копараторе, чтобы это работало быстрее?

Комментарии (29)


  1. iCpu
    21.12.2018 06:08

    Почему всего лишь -O1? Хотя бы уж -Ofast -ftree-vectorize -msse2
    Код для вектора тоже было бы вежливо предоставить. А так мы сравниваем бульдога с носорогом.


    1. onegreyonewhite Автор
      21.12.2018 06:59

      Код для вектора тоже было бы вежливо предоставить. А так мы сравниваем бульдога с носорогом. Погодите-ка… Это сравнение питоновых list и vector?

      У Python нет vector. Это биндинг из Cython, который берет из
      <vector>
      класс vector
      оригинальный код из Cython
      cdef extern from "<vector>" namespace "std" nogil:
          cdef cppclass vector[T,ALLOCATOR=*]:
              ctypedef T value_type
              ctypedef ALLOCATOR allocator_type
      
              # these should really be allocator_type.size_type and
              # allocator_type.difference_type to be true to the C++ definition
              # but cython doesn't support deferred access on template arguments
              ctypedef size_t size_type
              ctypedef ptrdiff_t difference_type
      
              cppclass iterator:
                  T& operator*()
                  iterator operator++()
                  iterator operator--()
                  iterator operator+(size_type)
                  iterator operator-(size_type)
                  difference_type operator-(iterator)
                  bint operator==(iterator)
                  bint operator!=(iterator)
                  bint operator<(iterator)
                  bint operator>(iterator)
                  bint operator<=(iterator)
                  bint operator>=(iterator)
              cppclass reverse_iterator:
                  T& operator*()
                  reverse_iterator operator++()
                  reverse_iterator operator--()
                  reverse_iterator operator+(size_type)
                  reverse_iterator operator-(size_type)
                  difference_type operator-(reverse_iterator)
                  bint operator==(reverse_iterator)
                  bint operator!=(reverse_iterator)
                  bint operator<(reverse_iterator)
                  bint operator>(reverse_iterator)
                  bint operator<=(reverse_iterator)
                  bint operator>=(reverse_iterator)
              cppclass const_iterator(iterator):
                  pass
              cppclass const_reverse_iterator(reverse_iterator):
                  pass
              vector() except +
              vector(vector&) except +
              vector(size_type) except +
              vector(size_type, T&) except +
              #vector[input_iterator](input_iterator, input_iterator)
              T& operator[](size_type)
              #vector& operator=(vector&)
              bint operator==(vector&, vector&)
              bint operator!=(vector&, vector&)
              bint operator<(vector&, vector&)
              bint operator>(vector&, vector&)
              bint operator<=(vector&, vector&)
              bint operator>=(vector&, vector&)
              void assign(size_type, const T&)
              void assign[input_iterator](input_iterator, input_iterator) except +
              T& at(size_type) except +
              T& back()
              iterator begin()
              const_iterator const_begin "begin"()
              size_type capacity()
              void clear()
              bint empty()
              iterator end()
              const_iterator const_end "end"()
              iterator erase(iterator)
              iterator erase(iterator, iterator)
              T& front()
              iterator insert(iterator, const T&) except +
              iterator insert(iterator, size_type, const T&) except +
              iterator insert[Iter](iterator, Iter, Iter) except +
              size_type max_size()
              void pop_back()
              void push_back(T&) except +
              reverse_iterator rbegin()
              const_reverse_iterator const_rbegin "crbegin"()
              reverse_iterator rend()
              const_reverse_iterator const_rend "crend"()
              void reserve(size_type)
              void resize(size_type) except +
              void resize(size_type, T&) except +
              size_type size()
              void swap(vector&)
      
              # C++11 methods
              T* data()
              void shrink_to_fit()
      


      1. iCpu
        21.12.2018 07:17

        У Python нет vector. Это биндинг из Cython
        А я повторю вопрос: причём тут C++? Зачем использовать Cython без его использования? Вопрос это не такой простой и глупый, как кажется. Что было бы, если бы вы не контейнер втянули, а сам словарь написали на С++, а в питон пробросили методы?
        Я описал почему мы используем так.
        Нет, не описал. «Мы используем -O1» — это не аргумент.


        1. onegreyonewhite Автор
          21.12.2018 08:43

          Нет, не описал. «Мы используем -O1» — это не аргумент.

          Наверно мало в тексте было написано:
          Очень хочется, чтобы можно было собирать *.whl для проекта и чтобы это всё завелось на практически любой системе

          Были проблемы при установке на машинах со слегка отличной архитектурой, поэтому поставили флаг -O1. В любом случае, наличие большей оптимизации производительности не принесло или добавило её незначительно (меньше 0.01с), поэтому мы используем для сборки такой уровень оптимизации. Как я уже написал в тексте, была цель сделать максимально приближено к условиям реальной эксплуатации.

          Что было бы, если бы вы не контейнер втянули, а сам словарь написали на С++, а в питон пробросили методы?

          Это вот вопрос уже интереснее, но не думаю, что профит был бы намного больше. Хотя думаю стоит проверить и такой вариант, для чистоты эксперимента. Впрочем, такой вариант использовать станет очень неудобно, поэтому будет не соблюдено главное требование — удобство при разработке. Т.к. этот код работать будет в библиотеке, то наверняка его захочется потом перегрузить и т.п. В случае с python/cython это решается просто, а с чистым C++ будет много лишней возни, как, например, наследование и т.п. Можно конечно только одну функцию вставки/обработки/сортировки использовать, но не настолько большой профит будет (да и не факт, что будет).


          1. Sazonov
            21.12.2018 11:16

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


            1. onegreyonewhite Автор
              22.12.2018 03:45

              До этого было -O3. Решили плавно наращивать и смотреть по отзывам. Разницы в целом не было, кроме -O0.


          1. iCpu
            21.12.2018 11:48

            Были проблемы при установке на машинах со слегка отличной архитектурой
            Если вы не планируете разворачиваться на Pentium 2, проблем быть не должно. По крайней мере, на актуальных архитектурах, включая атомы.
            не думаю, что профит был бы намного больше
            Зависит от квалификации программиста и сценариев использования. Тут у недавнего поста уравнение Френеля для миллиона нормалей считается 3-5 секунд. habr.com/company/otus/blog/433588

            Ну, вот, смотрите, берём самый тупой пример, переписанный с вашего. Я использую старые версии компилятора, в которых нет any, поэтому данные будут храниться либо в map<string,string>, либо в структуре.
            Исходники
            struct Dict 
            {
                std::string name;
                int test_data;
                std::string test_data2;
                std::vector<int> test_data3;
                std::map<std::string,std::string> properties;
            };
            
            typedef std::vector<std::map<std::string,std::string>> dictionary;
            typedef std::vector<Dict> dictionary2;
            
            int range_attempts = 1000000;
            
            std::vector<int> range(int N) {
                std::vector<int> v (N, 0);
                for (int i = 1; i < N; ++i) {
                    v[i] = i;
                }
                return v;
            }
            
            void runtest(dictionary & dict)
            {
                long long a, b;
                QElapsedTimer timer;
                dict.clear();
                timer.start();
                dict.assign(range_attempts, std::map<std::string,std::string>());
                for (int i = 0; i < range_attempts; ++i) {
                    dict[i]["name"      ] = "test_" + std::to_string(i);
                    dict[i]["test_data" ] = std::to_string(i);
                    dict[i]["test_data2"] = std::to_string(i);
                    dict[i]["test_data3"] = std::to_string(i);
                }
                a = timer.elapsed();
                dict.clear();
                b = timer.elapsed();
                qDebug() << a * 0.001 << b * 0.001;
            }
            
            void runtest2(dictionary2 & dict)
            {
                long long a, b;
                QElapsedTimer timer;
                dict.clear();
                timer.start();
                dict.assign(range_attempts, Dict());
                for (int i = 0; i < range_attempts; ++i) {
                    dict[i].name       = "test_" + std::to_string(i);
                    dict[i].test_data  = i;
                    dict[i].test_data2 = std::to_string(i);
                    dict[i].test_data3 = range(10);
                }
                a = timer.elapsed();
                dict.clear();
                b = timer.elapsed();
                qDebug() << a * 0.001 << b * 0.001;
            }
            
            void test(QWidget *parent) 
            {
                dictionary dict;
                for (int i = 0; i < 10; ++i) {
                    runtest(dict);
                }
                dictionary2 dict2;
                for (int i = 0; i < 10; ++i) {
                    runtest2(dict2);
                }
            }
            


            1. Pappageno
              21.12.2018 14:55

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

              Ответ несостоятелен, автор имеет ввиду march, который никого отношения к O не имеет.

              Исходники

              Это не исходник, а неведомая портянка с хрен пойми зачем туда впихнутым Qt. При этом ладно бы core, но там gui.

              Собираем в gcc 4.8.*, запускаем, получаем в среднем 24.203 и 10.115 секунд для чисто строкового контейнера и структуры, соответственно.

              Вменяемость цифр крайне сомнительна.

              Берём MSVC Да, компилятор решает.

              Тезис несостоятелен, MSVC является самым слабым компилятором — это первое. Второе, определяющим тут является качество имплементации stdlib.

              Запускаем на Ryzen 3 2200G и видим уже 1.3 и 0.5 секунд для gcc и 1.05 и 0.39 секунд для MSVC.

              Как это соотносится с:
              запускаем, получаем в среднем 24.203 и 10.115


              Опять же, запустить портянку без заморочек не представляется возможным. Полезность её крайне сомнительна.


            1. onegreyonewhite Автор
              22.12.2018 03:49

              Так я и не говорил, что такие числа по времени будут у всех на всех процессорах. Я не совсем понял какие выводы следовало бы сделать?
              Но я все равно хочу попробовать вынести весь C++ код в .h и посмотреть как это повлияет.


    1. Pappageno
      21.12.2018 08:49

      -Ofast

      Это небезопасно.


      1. 0xd34df00d
        21.12.2018 21:57

        Чем?


        1. Pappageno
          22.12.2018 03:00

          Ну во-первых. Тут полностью неадекватная модерация — авторы не пропускают критику, а модераторов либо нет, либо они мотивированы поступать аналогично. Поэтому, в данном случае, этой некий нейтральный(с уклоном в сторону автора) ответ призванный пройти модерацию.

          Во-вторых, небезопасно это потому, что fast — это fast-math, который разрешает компилятору изменения порядка вычислений для плавучки, что небезопасно т.к. изменения порядка изменяет результаты вычислений.


          1. 0xd34df00d
            22.12.2018 04:17

            Я упустил, что -Ofast включает -ffast-math. Почему-то думал, что это -O3 плюс мелочёвка. Да, вы правы.


    1. 0xd34df00d
      21.12.2018 21:57
      +1

      Хотя бы уж -Ofast -ftree-vectorize -msse2

      Зачем -msse2, если можно -march=native?


  1. zolkko
    21.12.2018 10:36

    У вас vector<Py_Object *> создаётся через new, а освобождается через free().
    И соответственно PyDict_new создаётся, но Py_DECREF нет. По этому pyo3 рулит, но это не точно =]


    1. onegreyonewhite Автор
      22.12.2018 03:24

      Спасибо за заметку. В понедельник попробую это проверить в сочетании с тем, что предложил iCpu по поводу полного переноса кода с вектором в C++.


  1. Pappageno
    21.12.2018 12:55

    А ведь я не поленился. Самое интересное тут то, что питон я никогда в глаза не видел. К тому же мне непонятно — что там на С++ собрались люди писать, если они не то что С++ не знают — они даже какие-то базовые основы программирования не особо знают.

    Если кратко. Я взял этот код, благо википедия помогла его запустить. Я посмотрел на два сгенерированных листинга и увидел PyDict_SetItemString и PyDict_SetItem. Далее, я загуглил PyDict_SetItemString и оказалось, что эта функция создаёт пистон-объект из си-строки. Вторая же функция принимает пистон-строку, да мало того, что пистон-строку — он ещё «кэширует» все литералы.

    И что же я сделал? Сделал невозможное — поменял одно на другое. Генератор создал пистон-строку(что очевидно, т.к. в коде используется пистон-строка) и закэшировал(по принципу си) её. Всё аналогично генерации из питона.

    И о чудо — test_vector стала быстрее. Т.к. у вас никаких тестов нет — проверить валидность я не смог. В любом случае несостоятельность статьи нотариально доказана.

    Теперь по поводу общей глупости в статье. Ну во-первых, что измеряют эти тесты? Они измеряют вот это:

    dict(
                name = 'test_{}'.format(i),
                test_data = i,
                test_data2 = str(i),
                test_data3 = range(10),
            )


    И это очевидно. Какое отношение бенчмарк, где создание dict занимает 99% времени измеряет оверхед на list? Никаким. И во-вторых — тут никак не сослаться на ошибку, ведь проверяется это очень просто:

            data_list.append(dict(
                #name = 'test_{}'.format(i),
                test_data = i,
                #test_data2 = str(i),
                #test_data3 = range(10),
            ))
    


    И о чудо — код с вектором в 3раза быстрее.

    О таких «мелочах» как:

    data_list.resize(range_attempts)

    Я даже говорить не буду.


    1. iCpu
      21.12.2018 13:10

      Не то, чтобы я защищаю автора, — он сам за себя ответит, но статья как чисто прикладная задача очень интересна. Если нужно создавать объекты — их нужно создавать (привет, К.О.), а разные сценарии работы требуют разные способы инициализации данных. К примеру, правильное использование Cython требует наличие головы и рук, которые должны расти хотя бы из жопы, а не в неё. То же с тестами.

      Так что с некоторыми аргументами автора я полностью согласен: если на питоне он может это написать относительно качественно и быстро — не надо давать ему гранату, пусть и дальше пишет на питоне.


      1. Pappageno
        21.12.2018 14:31

        но статья как чисто прикладная задача очень интересна.

        Чем?

        Если нужно создавать объекты — их нужно создавать (привет, К.О.), а разные сценарии работы требуют разные способы инициализации данных.

        Что из этого следует и зачем это было написано? Кто-то говорил о какой-то инициализации данных? Нет.

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

        И? К чему это? Причём тут Cython?

        Так что с некоторыми аргументами автора я полностью согласен: если на питоне он может это написать относительно качественно и быстро — не надо давать ему гранату, пусть и дальше пишет на питоне.

        Никаких аргументов нет и небыло. Статья целиком и полностью несостоятельна — сравнение некорректно, выводы некорректны.


        1. onegreyonewhite Автор
          22.12.2018 03:33

          Скорее выводы из этого комментария несостоятельны, потому как я в статье очень конкретно обозначил, что цель написать не просто супероптимизировано, но так чтобы это укладывалось в рамках задачи: массив словарей.
          Если просто поотключать всё из кода, то он безусловно будет работать около 0мс, но зачем такой код нужен?


          Цель же статьи не сравнить C++ и Python, а сравнить возможность и необходимость решения задачи, используя те или иные возможности C++ расширения.


          1. Pappageno
            22.12.2018 03:42
            +1

            Пациент пошел ставить минусы всем моим комментам. Это настолько глупо.

            Скорее выводы из этого комментария несостоятельны

            Основания для этих нелепых попыток?

            что цель написать не просто супероптимизировано, но так чтобы это укладывалось в рамках задачи: массив словарей.

            К чему написан этот нелепый набор слов? Я где-то говорил о каких-то оптимизациях? Нет.

            Если просто поотключать всё из кода, то он безусловно будет работать около 0мс, но зачем такой код нужен?

            К чему написан этот нелепый набор слов? Я где-то говорил, что нужно что-то отключать? Нет. Я сообщил о несостоятельности бенчмарков и сравнения, причины были обозначены — есть что возразить — возражайте под оригинальным комментом, цитируя то, на что возражаете.

            Цель же статьи не сравнить C++ и Python, а сравнить возможность и необходимость решения задачи, используя те или иные возможности C++ расширения.

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


            1. onegreyonewhite Автор
              22.12.2018 06:10
              -1

              Пациент пошел ставить минусы всем моим комментам. Это настолько глупо.
              Глупо жаловаться на то, что вам ставят минусы. Возможно это связанно с тем, что вы вместо того, чтобы услышать людей, доказываете какую-то свою правоту:
              К чему написан очередной нелепый набор слов?

              Я сообщил о несостоятельности бенчмарков и сравнения… Ну во-первых, что измеряют эти тесты?.. И это очевидно. Какое отношение бенчмарк, где создание dict занимает 99% времени измеряет оверхед на list? Никаким.

              Кроме поверхностных выводов, вы статью читали (не только сам скрипт)? Вопрос стоял вполне себе конкретно: нужно было обработать один большой массив словарей (не map'ов, не структур, а конкретно словарей типа dict из Python), да и сделать это так, чтобы потом не пришлось танцевать с бубном, чтобы использовать этот массив в Python-коде. Цели переписать всё на C++ не было.
              Что из этого следует и зачем это было написано? Кто-то говорил о какой-то инициализации данных? Нет.

              А как работать с данными, если их нет? Даже если бы данные где-либо инициализировались до этого, то после потребовалось бы их всё равно туда складывать, поэтому задача приближена не к синтетическим тестам, а к реальной эксплуатации, то как оно реально будет использоваться.

              Более того, основной посыл статьи чётко обозначен:
              После этого эксперимента, мы решили сильно не заморачиваться с поддержкой Python 2.7 и переделкой каких-либо частей приложений на C++, просто потому что оно того не стоит. Всё уже написали до нас, нам остаётся это лишь правильно применить для решения конкретной задачи.

              Даже если мы всю реализацию переместим в C/C++ — это ничего не даст, потому что по прежнему мы будем работать в Python и оперировать его объектами. Для C/C++ есть своё место здесь — вести расчёты или полностью выносить критический функционал, однако это может стоить ещё дороже, ведь по факту, придётся делать то же, что уже реализовано целым сообществом разработчиков Python и не факт, что мы справимся лучше.


              1. Pappageno
                22.12.2018 07:52

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

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

                Кроме поверхностных выводов, вы статью читали (не только сам скрипт)? Вопрос стоял вполне себе конкретно: нужно было обработать один большой массив словарей

                Вы опять пытаетесь выдать голоса в своей голове за мои тезисы. Про «не словари» говорил вам не я, про оптимизации говорил вам не я.

                Моя претензия по поводу словарей состояла в том. Давайте я вам объясню попроще. Мы хотим сравнивать температуру чая и кофе. Как вы это делаете? Вы взяли два стакана с водой(при этом, как оказалось, разной температуры), капнули в один каплю чая, во второй каплю кофе и получили какие-то результаты. И вы пошли дальше, вы взяли не по одному, а по 4 стакана, слили их в ведро и произвели описанные выше манипуляции.

                Очевидно, что человек обладающей банальной логикой сможет осознать проблему происходящего, вернее всю нелепость происходящего. Тут не нужно знать питон, С++, нужно иметь элементарный кругозор доступный любому программисту, особенно тому, который пытается что-то измерять, что-то оценивать. Ничего более я не говорил.

                Т.е. вопрос звучит так. Если основная работа у вас в словарях, то зачем и для чего вы начали сравнивать вектор/питон-список? Какой в этой смысл?

                (не map'ов, не структур, а конкретно словарей типа dict из Python)

                Вы опять всё перепутали. Это было в другом комменте от другого человека. По поводу моих претензий — они заключались в том, что декларируя сравнения С++-вектора/питон-списка вы не сравнивали вектор/список, а сравнивали хрен пойми что.

                А как работать с данными, если их нет? Даже если бы данные где-либо инициализировались до этого, то после потребовалось бы их всё равно туда складывать, поэтому задача приближена не к синтетическим тестам, а к реальной эксплуатации, то как оно реально будет использоваться.

                Абсолютно нелепое заявление. В контексте сравнения вектора/списка никого не должно это интересовать. Пример со стаканами я вам привёл. К тому же, эта инициализация у вас была не идентична и даже тут вы засыпались.

                Более того, основной посыл статьи чётко обозначен:

                И? Это выводы основанные на несостоятельных измерениях и интерпретациях. Да и сама постановка задачи нелепа. Давайте за секунду пройдём по вектору указателей 1кк раз.

                Ладно, я могу предположить, что вы не знали о том, что эта задача исполняется на порядки быстрее, а значит время отработки логики вектора в задаче единицы, доли процента. Очевидно, что если мы ускорим список в 10раз — мы ускорим программу на 10%, но это не значит, что список равен вектору и наоборот.

                Тут куда не глянь везде треш. Т.е. просто непонятно как вы пришли к подобным выводам, причём тут вектор, причём тут это сравнение. Зачем говорить о векторе, а потом съезжать «нам неважно что там с вектором». Слишком сложно для меня.

                Даже если мы всю реализацию переместим в C/C++ — это ничего не даст, потому что по прежнему мы будем работать в Python и оперировать его объектами.

                Вы уже всю реализацию переместили в С/С++. В этом суть cython. Далее уже сравнивается нативные для С/С++ решения, либо другие решения на С/С++ созданные для питона.

                Кстати, в понятие «всю» не входит никакой питон. Я могу предположить, что в cython можно реализовать какие-то модули для обычного питона и через это интегрировать их с уже существующем кодом на питоне.

                Для C/C++ есть своё место здесь — вести расчёты или полностью выносить критический функционал

                Где здесь? В мире питона? Ну дак вы говорили не об этом, вы утверждали, что «использование С++ ничего не даёт»(зачем вам тогда cython, если оно ничего не даёт? Или вы про ручное переписывание? Ну дак зачем в cython этот функционал? Его авторы идиоты и делают ненужно?). Теперь вы утверждаете обратное.

                однако это может стоить ещё дороже

                Это как в случае выше? Очень весомый аргумент.

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

                Это очень глупый тезис. Разработчики питона реализовывали логику языка питон, его примитивов и прочего. Очевидно, что для решения любой дачи не нужны эти примитивы и вы сами это подтвердили. Вы можете использовать питон-лист, а можете С++-вектор.

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

                К тому же, у «разработчики С++» так же не глупые и могут написать нормальный список, нормальную мапу и прочее. Только тут нет тех ограничений, которые есть в пистоне(как и любом скриптовом языке).

                Я вообще первый раз вижу питон, но посмотрев на описание pylong я увидел, что там каике-то 256битные числа(или что-то типа того). Я посмотрел на строки, которые в utf-8. Очевидно, что подобные решения удобны для некоего общего случая использования, но они чего-то стоят, они очень много стоят.

                Нужны ли вам в ключах utf8? Сомневаюсь. Нужны ли вам числа шире 64бит? Сомневаюсь. И абсолютно неважно, кто является разработчиков питона — написать быстрее utf8-строки, либо 256битные числа — невозможно. Под быстрыми имеется ввиду относительно базовых.

                В этом смысл С/С++-решений. Вы не платит за то, что вам ненужно. Вам нужны 256битные числа? Пожалуйста, какие угодно. Не нужны? Берите базовые и быстрее. Нужны utf8строки? Берите. Не нужны — берите быстрее базовые.


    1. onegreyonewhite Автор
      22.12.2018 06:11
      -1

      Если кратко. Я взял этот код, благо википедия помогла его запустить. Я посмотрел на два сгенерированных листинга и увидел PyDict_SetItemString и PyDict_SetItem. Далее, я загуглил PyDict_SetItemString и оказалось, что эта функция создаёт пистон-объект из си-строки. Вторая же функция принимает пистон-строку, да мало того, что пистон-строку — он ещё «кэширует» все литералы.

      Вот это интересно. Надо будет в пн попробовать добавить в скрипт и посмотреть что выйдет.


  1. iroln
    21.12.2018 15:52
    +1

    from libc.stdlib cimport free
    ...
    from libcpp.vector cimport vector
    
    ...
    
    cdef dict_vec *data_list
    data_list = new dict_vec()
    ...
    free(data_list)

    Если C++ не знаете, идёте в документацию по Cython и читаете:


    C++ objects can be dynamically allocated with new and del keywords.


    1. onegreyonewhite Автор
      22.12.2018 03:35

      Намёк на то, что можно было вместо free() использовать del data_list? Есть ли от этого какая-то практическая польза?


      1. iroln
        22.12.2018 03:40

        Нельзя использовать free для удаления объектов, созданных через new! free нужно использовать только для освобождения памяти, выделенной через malloc. Это же азы, ну как так то, ребята?


        И вообще, я не вижу никакой практической пользы в создании объекта вектора динамически. Зачем вы вообще это делаете?


        1. onegreyonewhite Автор
          22.12.2018 06:14

          И вообще, я не вижу никакой практической пользы в создании объекта вектора динамически. Зачем вы вообще это делаете?

          Эксперимент. В рамках Django это нормальное явление, поэтому так и сделали.
          Нельзя использовать free для удаления объектов, созданных через new! free нужно использовать только для освобождения памяти, выделенной через malloc. Это же азы, ну как так то, ребята?

          Да, это мой косяк в силу недостатка знания. В пн попробую реализовать через malloc и обновлю статью.


          1. iroln
            23.12.2018 17:12
            +1

            Да, это мой косяк в силу недостатка знания. В пн попробую реализовать через malloc и обновлю статью.

            Зачем malloc?! Зачем пытаться создавать вектор динамически? std::vector сам управляет памятью под свои элементы, это по сути динамический массив, вам не надо заморачиваться вообще с управлением памятью для этого контейнера и создавать его объект в куче. Просто создаёте объект вектора на стеке и резервируете необходимое количество памяти под количество элементов, которые вам надо хранить, с помощью vector.reserve и всё.