Структура данных C для имитации списка C # <List <int>>? - PullRequest
5 голосов
/ 05 декабря 2008

Я пытаюсь реорганизовать метод c # в функцию c, пытаясь набрать некоторую скорость, а затем вызвать c dll в c #, чтобы позволить моей программе использовать эту функциональность.

В настоящее время метод c # берет список целых чисел и возвращает список списков целых чисел. Метод вычислил набор мощностей целых чисел, поэтому при вводе 3-х целых будет получен следующий результат (на данном этапе значения в целых значениях не важны, так как он используется в качестве внутреннего значения веса)

1
2
3
1,2
1,3
2,3
1,2,3

Где каждая строка представляет список целых чисел. Выходные данные указывают индекс (со смещением 1) первого списка, а не значение. 1,2 означает, что элемент с индексами 0 и 1 является элементом набора мощности.

Я не знаком с c, так каковы мои лучшие варианты структур данных, которые позволят c # получить доступ к возвращенным данным?

Заранее спасибо

Обновление

Спасибо всем за ваши комментарии. Вот немного предыстории характера проблемы.

Итерационный метод вычисления набора мощностей множества довольно прост. Два цикла и немного битовых манипуляций - вот и все, что нужно. Его просто называют ... много (на самом деле, миллиарды раз, если размер набора достаточно велик).

Я думаю, что использование c (c ++, как отмечали люди) заключается в том, что он дает больше возможностей для настройки производительности. Прямой порт может не предлагать никакого увеличения, но он открывает путь для более сложных методов, чтобы получить немного большую скорость из этого. Даже небольшое увеличение за итерацию будет равносильно измеримому увеличению.

Моя идея состояла в том, чтобы портировать прямую версию, а затем работать над ее увеличением. А затем реорганизовать его с течением времени (с помощью всех, кто здесь, в SO).

Обновление 2

Еще одно справедливое замечание от jalf: мне не нужно использовать list или equivelent. Если есть лучший способ, тогда я открыт для предложений. Единственной причиной для списка было то, что каждый набор результатов не имеет одинаковый размер.

Код пока ...

public List<List<int>> powerset(List<int> currentGroupList)
{
    _currentGroupList = currentGroupList;
    int max;
    int count;

    //Count the objects in the group
    count = _currentGroupList.Count;
    max = (int)Math.Pow(2, count);

    //outer loop
    for (int i = 0; i < max; i++)
    {
        _currentSet = new List<int>();

        //inner loop
        for (int j = 0; j < count; j++)
        {              
            if ((i & (1 << j)) == 0)
            {
                _currentSetList.Add(_currentGroupList.ElementAt(j));                          
            }
        }
        outputList.Add(_currentSetList);
    }   
    return outputList;
}

Как видите, не так много. Это просто крутится вокруг!

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

Обновление 2

Спасибо за всю работу по вводу и реализации. Просто чтобы прояснить пару затронутых моментов: мне не нужно, чтобы выходные данные были в «естественном порядке», а также меня не интересует возвращаемое пустое множество.

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

Тогда возникает вопрос: достигли ли мы максимальной скорости для этого вычисления в C #? Предоставляет ли опция неуправляемого кода какие-либо дополнительные возможности. Я знаю, что во многих отношениях ответ бесполезен, так как даже если бы у нас было мало времени для запуска, это позволило бы только дополнительные значения в исходном наборе.

Ответы [ 10 ]

10 голосов
/ 05 декабря 2008

Кроме того, убедитесь, что переход на C / C ++ - это действительно то, что вам нужно, чтобы начать с скорости. Изучите оригинальный метод C # (автономный, выполняемый с помощью модульных тестов), инструментальный новый метод C / C ++ (опять же, автономный с помощью модульных тестов) и посмотрите, в чем разница в реальном мире.

Причина, по которой я привел это, состоит в том, что я боюсь, что это может быть пирровой победой - используя совет Смоки Бэкона, вы получаете свой список классов, вы находитесь на «более быстром» C ++, но все еще стоит платить за вызов этой DLL Отказ от времени выполнения с помощью P / Invoke или COM-взаимодействия влечет за собой довольно существенные затраты производительности.

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

Обновление на основе обновления ОП

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

Я думаю, учитывая описание проблемы, проблема не в том, что C # / .NET "медленнее", чем в C, а скорее в том, что код необходимо оптимизировать. Как уже упоминалось в другом постере, вы можете использовать указатели в C #, чтобы серьезно повысить производительность в этом цикле без необходимости маршалинга. Я сначала изучу это, прежде чем прыгнуть в сложный мир взаимодействия, для этого сценария.

8 голосов
/ 05 декабря 2008

Если вы хотите использовать C для повышения производительности, скорее всего, вы планируете сделать это с помощью указателей. C # позволяет использовать указатели, используя ключевое слово unsafe. Вы рассматривали это?

Кроме того, как вы будете называть этот код ... будет ли он часто вызываться (например, в цикле?). Если это так, распределение данных вперед и назад может более чем компенсировать любое повышение производительности.


Follow Up

Взгляните на Собственный код без ущерба для производительности .NET для некоторых опций взаимодействия. Существуют способы взаимодействия без особых потерь производительности, но эти взаимодействия могут происходить только с простейшими типами данных.

Хотя я все еще думаю, что вам следует изучить возможность ускорения вашего кода с использованием прямой .NET.


Follow 2

Кроме того, позвольте мне предложить, чтобы, если вы настроены на смешивание нативного и управляемого кода, вы создали свою библиотеку с помощью c ++ / cli. Ниже приведен простой пример. Обратите внимание, что я не работаю в C ++ / Cli, и этот код не делает ничего полезного ... он просто показывает, как легко вы можете смешивать нативный и управляемый код.

#include "stdafx.h"

using namespace System;

System::Collections::Generic::List<int> ^MyAlgorithm(System::Collections::Generic::List<int> ^sourceList);


int main(array<System::String ^> ^args)
{
    System::Collections::Generic::List<int> ^intList = gcnew System::Collections::Generic::List<int>();

    intList->Add(1);
    intList->Add(2);
    intList->Add(3);
    intList->Add(4);
    intList->Add(5);

    Console::WriteLine("Before Call");
    for each(int i in intList)
    {
        Console::WriteLine(i);
    }

    System::Collections::Generic::List<int> ^modifiedList = MyAlgorithm(intList);

    Console::WriteLine("After Call");
    for each(int i in modifiedList)
    {
        Console::WriteLine(i);
    }
}


System::Collections::Generic::List<int> ^MyAlgorithm(System::Collections::Generic::List<int> ^sourceList)
{
    int* nativeInts = new int[sourceList->Count];

    int nativeIntArraySize = sourceList->Count;

    //Managed to Native
    for(int i=0; i<sourceList->Count; i++)
    {
        nativeInts[i] = sourceList[i];
    }

    //Do Something to native ints
    for(int i=0; i<nativeIntArraySize; i++)
    {
        nativeInts[i]++;
    }


    //Native to Managed
    System::Collections::Generic::List<int> ^returnList = gcnew System::Collections::Generic::List<int>();
    for(int i=0; i<nativeIntArraySize; i++)
    {
        returnList->Add(nativeInts[i]);
    }


    return returnList;
}
7 голосов
/ 05 декабря 2008

Что заставляет вас думать, что вы наберете скорость, вызывая код C? С магически не быстрее, чем С #. Это может быть, конечно, но это также может быть медленнее (и хуже). Особенно, когда вы учитываете вызовы p / invoke в нативном коде, далеко не уверен, что этот подход ускорит что-либо.

В любом случае, C не имеет ничего подобного List. Он имеет необработанные массивы и указатели (и вы можете утверждать, что int ** более или менее эквивалентен), но вам, вероятно, лучше использовать C ++, который имеет эквивалентные структуры данных. В частности, std :: vector. Однако не существует простых способов представить эти данные в C #, поскольку они будут разбросаны в значительной степени случайным образом (каждый список является указателем на некоторую динамически распределенную память где-то )

Тем не менее, я подозреваю, что наибольшее улучшение производительности связано с улучшением алгоритма в C #.

Edit:

Я вижу в вашем алгоритме несколько вещей, которые кажутся неоптимальными. Создание списка списков не является бесплатным. Возможно, вы можете создать один список и использовать разные смещения для представления каждого подсписка. Или, возможно, использование 'yield return' и IEnumerable вместо явного построения списков может быть быстрее.

Вы профилировали свой код, выяснили, где время тратится?

5 голосов
/ 08 декабря 2008

Возвращает один набор блоков питания за раз. Он основан на коде Python здесь . Он работает для блоков питания более 32 элементов. Если вам нужно меньше 32, вы можете изменить long на int. Это довольно быстро - быстрее, чем мой предыдущий алгоритм, и быстрее, чем (мой модифицированный для использования версии с возвратом-возвращением) код P Paddy.

static class PowerSet4<T>
{
    static public IEnumerable<IList<T>> powerset(T[] currentGroupList)
    {
        int count = currentGroupList.Length;
        Dictionary<long, T> powerToIndex = new Dictionary<long, T>();
        long mask = 1L;
        for (int i = 0; i < count; i++)
        {
            powerToIndex[mask] = currentGroupList[i];
            mask <<= 1;
        }

        Dictionary<long, T> result = new Dictionary<long, T>();
        yield return result.Values.ToArray();

        long max = 1L << count;
        for (long i = 1L; i < max; i++)
        {
            long key = i & -i;
            if (result.ContainsKey(key))
                result.Remove(key);
            else
                result[key] = powerToIndex[key];
            yield return result.Values.ToArray();
        }
    }
}

Вы можете скачать все самые быстрые версии, которые я тестировал здесь .

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

5 голосов
/ 05 декабря 2008

Я также собираюсь проголосовать за настройку вашего C #, в частности, перейдя к «небезопасному» коду и потеряв, что может быть слишком много накладных расходов на проверку границ.

Несмотря на то, что это «небезопасно», оно не менее «безопасно», чем C / C ++, и его намного проще понять.

3 голосов
/ 07 декабря 2008

Ниже приведен алгоритм на C #, который должен быть намного быстрее (и использовать меньше памяти), чем алгоритм, который вы опубликовали. Он не использует аккуратный бинарный трюк, используемый вами, и, как следствие, код становится длиннее. У него на несколько for циклов больше, чем у вас, и может потребоваться время или два, чтобы пройти его с отладчиком, чтобы полностью его прогнать. Но на самом деле это более простой подход, когда вы понимаете, что он делает.

В качестве бонуса возвращаемые наборы находятся в более «естественном» порядке. Он вернет подмножества набора {1 2 3} в том же порядке, в котором вы перечислили их в своем вопросе. Это не фокус, но побочный эффект используемого алгоритма.

В моих тестах я обнаружил, что этот алгоритм примерно в 4 раза быстрее, чем алгоритм, который вы опубликовали для большого набора из 22 элементов (который был настолько большим, насколько я мог использовать на своей машине, без чрезмерного разбивания диска и искажения результатов много). Одна ваша попытка заняла около 15,5 секунд, а моя - около 3,6 секунд.

Для небольших списков разница менее выражена. Для набора только из 10 предметов ваш бегал 10 000 раз за 7,8 секунды, а мой - около 3,2 секунды. Для наборов с 5 или менее предметами они бегут близко к одному и тому же времени. Со многими итерациями ваша работа выполняется немного быстрее.

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

/* 
 * Made it static, because it shouldn't really use or modify state data.
 * Making it static also saves a tiny bit of call time, because it doesn't
 * have to receive an extra "this" pointer.  Also, accessing a local
 * parameter is a tiny bit faster than accessing a class member, because
 * dereferencing the "this" pointer is not free.
 * 
 * Made it generic so that the same code can handle sets of any type.
 */
static IList<IList<T>> PowerSet<T>(IList<T> set){
    if(set == null)
        throw new ArgumentNullException("set");

    /*
     * Caveat:
     * If set.Count > 30, this function pukes all over itself without so
     * much as wiping up afterwards.  Even for 30 elements, though, the
     * result set is about 68 GB (if "set" is comprised of ints).  24 or
     * 25 elements is a practical limit for current hardware.
     */
    int   setSize     = set.Count;
    int   subsetCount = 1 << setSize; // MUCH faster than (int)Math.Pow(2, setSize)
    T[][] rtn         = new T[subsetCount][];
    /* 
     * We don't really need dynamic list allocation.  We can calculate
     * in advance the number of subsets ("subsetCount" above), and
     * the size of each subset (0 through setSize).  The performance
     * of List<> is pretty horrible when the initial size is not
     * guessed well.
     */

    int subsetIndex = 0;
    for(int subsetSize = 0; subsetSize <= setSize; subsetSize++){
        /*
         * The "indices" array below is part of how we implement the
         * "natural" ordering of the subsets.  For a subset of size 3,
         * for example, we initialize the indices array with {0, 1, 2};
         * Later, we'll increment each index until we reach setSize,
         * then carry over to the next index.  So, assuming a set size
         * of 5, the second iteration will have indices {0, 1, 3}, the
         * third will have {0, 1, 4}, and the fifth will involve a carry,
         * so we'll have {0, 2, 3}.
         */
        int[] indices = new int[subsetSize];
        for(int i = 1; i < subsetSize; i++)
            indices[i] = i;

        /*
         * Now we'll iterate over all the subsets we need to make for the
         * current subset size.  The number of subsets of a given size
         * is easily determined with combination (nCr).  In other words,
         * if I have 5 items in my set and I want all subsets of size 3,
         * I need 5-pick-3, or 5C3 = 5! / 3!(5 - 3)! = 10.
         */
        for(int i = Combination(setSize, subsetSize); i > 0; i--){
            /*
             * Copy the items from the input set according to the
             * indices we've already set up.  Alternatively, if you
             * just wanted the indices in your output, you could
             * just dup the index array here (but make sure you dup!
             * Otherwise the setup step at the bottom of this for
             * loop will mess up your output list!  You'll also want
             * to change the function's return type to
             * IList<IList<int>> in that case.
             */
            T[] subset = new T[subsetSize];
            for(int j = 0; j < subsetSize; j++)
                subset[j] = set[indices[j]];

            /* Add the subset to the return */
            rtn[subsetIndex++] = subset;

            /*
             * Set up indices for next subset.  This looks a lot
             * messier than it is.  It simply increments the
             * right-most index until it overflows, then carries
             * over left as far as it needs to.  I've made the
             * logic as fast as I could, which is why it's hairy-
             * looking.  Note that the inner for loop won't
             * actually run as long as a carry isn't required,
             * and will run at most once in any case.  The outer
             * loop will go through as few iterations as required.
             * 
             * You may notice that this logic doesn't check the
             * end case (when the left-most digit overflows).  It
             * doesn't need to, since the loop up above won't
             * execute again in that case, anyway.  There's no
             * reason to waste time checking that here.
             */
            for(int j = subsetSize - 1; j >= 0; j--)
                if(++indices[j] <= setSize - subsetSize + j){
                    for(int k = j + 1; k < subsetSize; k++)
                        indices[k] = indices[k - 1] + 1;
                    break;
                }
        }
    }
    return rtn;
}

static int Combination(int n, int r){
    if(r == 0 || r == n)
        return 1;

    /*
     * The formula for combination is:
     *
     *       n!
     *   ----------
     *   r!(n - r)!
     *
     * We'll actually use a slightly modified version here.  The above
     * formula forces us to calculate (n - r)! twice.  Instead, we only
     * multiply for the numerator the factors of n! that aren't canceled
     * out by (n - r)! in the denominator.
     */

    /*
     * nCr == nC(n - r)
     * We can use this fact to reduce the number of multiplications we
     * perform, as well as the incidence of overflow, where r > n / 2
     */
    if(r > n / 2) /* We DO want integer truncation here (7 / 2 = 3) */
        r = n - r;

    /*
     * I originally used all integer math below, with some complicated
     * logic and another function to handle cases where the intermediate
     * results overflowed a 32-bit int.  It was pretty ugly.  In later
     * testing, I found that the more generalized double-precision
     * floating-point approach was actually *faster*, so there was no
     * need for the ugly code.  But if you want to see a giant WTF, look
     * at the edit history for this post!
     */

    double denominator = Factorial(r);
    double numerator   = n;
    while(--r > 0)
        numerator *= --n;

    return (int)(numerator / denominator + 0.1/* Deal with rounding errors. */);
}

/*
 * The archetypical factorial implementation is recursive, and is perhaps
 * the most often used demonstration of recursion in text books and other
 * materials.  It's unfortunate, however, that few texts point out that
 * it's nearly as simple to write an iterative factorial function that
 * will perform better (although tail-end recursion, if implemented by
 * the compiler, will help to close the gap).
 */
static double Factorial(int x){
    /*
     * An all-purpose factorial function would handle negative numbers
     * correctly - the result should be Sign(x) * Factorial(Abs(x)) -
     * but since we don't need that functionality, we're better off
     * saving the few extra clock cycles it would take.
     */

    /*
     * I originally used all integer math below, but found that the
     * double-precision floating-point version is not only more
     * general, but also *faster*!
     */

    if(x < 2)
        return 1;

    double rtn = x;
    while(--x > 1)
        rtn *= x;

    return rtn;
}
2 голосов
/ 07 декабря 2008

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

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

static class PowerSet<T>
{
    static long[] mask = { 1L << 0, 1L << 1, 1L << 2, 1L << 3, 
                           1L << 4, 1L << 5, 1L << 6, 1L << 7, 
                           1L << 8, 1L << 9, 1L << 10, 1L << 11, 
                           1L << 12, 1L << 13, 1L << 14, 1L << 15, 
                           1L << 16, 1L << 17, 1L << 18, 1L << 19, 
                           1L << 20, 1L << 21, 1L << 22, 1L << 23, 
                           1L << 24, 1L << 25, 1L << 26, 1L << 27, 
                           1L << 28, 1L << 29, 1L << 30, 1L << 31};
    static public IEnumerable<IList<T>> powerset(T[] currentGroupList)
    {
        int count = currentGroupList.Length;
        long max = 1L << count;
        for (long iter = 0; iter < max; iter++)
        {
            T[] list = new T[count];
            int k = 0, m = -1;
            for (long i = iter; i != 0; i &= (i - 1))
            {
                while ((mask[++m] & i) == 0)
                    ;
                list[k++] = currentGroupList[m];
            }
            yield return list;
        }
    }
}

Тогда ваш код клиента выглядит так:

    static void Main(string[] args)
    {
        int[] intList = { 1, 2, 3, 4 };
        foreach (IList<int> set in PowerSet<int>.powerset(intList))
        {
            foreach (int i in set)
                Console.Write("{0} ", i);
            Console.WriteLine();
        }
    }

Я даже бесплатно добавлю алгоритм переворота с шаблонными аргументами. Для дополнительной скорости вы можете заключить внутренний цикл powerlist () в небезопасный блок. Это не имеет большого значения.

На моей машине этот код немного медленнее, чем код OP, пока наборы не станут 16 или больше. Тем не менее, все время до 16 элементов менее 0,15 секунды. При 23 элементах он работает в 64% случаев. Исходный алгоритм не работает на моей машине для 24 или более элементов - ему не хватает памяти.

Этот код занимает 12 секунд для генерации мощности, установленной для чисел от 1 до 24, без учета времени ввода / вывода с экрана. Это 16 миллионов раз в 12 секунд, или около 1400K в секунду. Для миллиарда (это то, что вы цитировали ранее), это будет около 760 секунд. Как вы думаете, сколько времени это займет?

1 голос
/ 07 декабря 2008

Я согласен с мнением «сначала оптимизируй .NET». Это самый безболезненный. Я полагаю, что если бы вы написали некоторый неуправляемый код .NET с использованием указателей C #, он был бы идентичен выполнению на C, за исключением затрат на виртуальную машину.

1 голос
/ 05 декабря 2008

Это должен быть C или C ++ тоже вариант? Если C ++, вы можете просто использовать свой собственный тип list из STL. В противном случае вам придется реализовать свой собственный список - искать связанные списки или массивы динамического размера для указателей того, как это сделать.

0 голосов
/ 07 декабря 2008

P Папочка:

Вы можете изменить свой код комбинации () следующим образом:

    static long Combination(long n, long r)
    {
        r = (r > n - r) ? (n - r) : r;
        if (r == 0)
            return 1;
        long result = 1;
        long k = 1;
        while (r-- > 0)
        {
            result *= n--;
            result /= k++;
        }

        return result;
    }

Это уменьшит умножения и вероятность переполнения до минимума.

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