为什么将此代码段从C#转换为C ++会降低性能?

时间:2017-08-17 22:57:05

标签: c# c++ algorithm optimization suffix-tree

我比C ++更熟悉C#,所以我必须就此问题寻求建议。我不得不将一些代码片段重写为C ++,然后(令人惊讶地)遇到了性能问题。

我已将问题缩小到这些片段:

C#

   public class SuffixTree
    {
        public class Node
        {
            public int Index = -1;
            public Dictionary<char, Node> Children = new Dictionary<char, Node>();
        }

        public Node Root = new Node();
        public String Text;

        public SuffixTree(string s)
        {
            Text = s;
            for (var i = s.Length - 1; i >= 0; --i)
                InsertSuffix(s, i);
        }

        public void InsertSuffix(string s, int from)
        {
            var cur = Root;
            for (int i = from; i < s.Length; ++i)
            {
                var c = s[i];
                if (!cur.Children.ContainsKey(c))
                {
                    var n = new Node() { Index = from };
                    cur.Children.Add(c, n);

                    return;
                }
                cur = cur.Children[c];
            }
        }

        public bool Contains(string s)
        {
            return FindNode(s) != null;
        }

        private Node FindNode(string s)
        {
            var cur = Root;
            for (int i = 0; i < s.Length; ++i)
            {
                var c = s[i];
                if (!cur.Children.ContainsKey(c))
                {
                    for (var j = i; j < s.Length; ++j)
                        if (Text[cur.Index + j] != s[j])
                            return null;
                    return cur;
                }
                cur = cur.Children[c];
            }
            return cur;
        }
    }
}

C ++

struct node
{
    int index;
    std::unordered_map<char, node*> children;

    node() { this->index = -1; }
    node(int idx) { this->index = idx; }
};

struct suffixTree
{
    node* root;
    char* text;

    suffixTree(char* str)
    {
        int len = strlen(str) + 1;
        this->text = new char[len];
        strncpy(this->text, str, len);

        root = new node();
        for (int i = len - 2; i >= 0; --i)
            insertSuffix(str, i);
    }

    void insertSuffix(char* str, int from)
    {
        node* current = root;
        for (int i = from; i < strlen(str); ++i)
        {
            char key = str[i];
            if (current->children.find(key) == current->children.end())
            {
                current->children[key] = new node(from);
                return;
            }
            current = current->children[key];
        }
    }

    bool contains(char* str)
    {
        node* current = this->root;
        for (int i = 0; i < strlen(str); ++i)
        {
            char key = str[i];
            if (current->children.find(key) == current->children.end())
            {
                for (int j = i; j < strlen(str); ++j)
                    if (this->text[current->index + j] != str[j])
                        return false;
                return true;
            }
            current = current->children[key];
        }
    }
}

在这两种情况下,我都会创建一个后缀树,然后在一个更大的函数中使用它,这个函数与post无关(让我们称之为F())。我已经测试了两个随机生成的长度为100000的字符串.C#版本构造了我的后缀树并在F()中使用它的总执行时间为: 480 ms 而代码是我已经&#34;翻译成C ++&#34;执行 48秒

我已经深入研究了这一点,似乎在我的C ++代码中,构造函数需要 47秒,而使用F()中的树运行 48 ms 比C#快10倍。

结论

似乎主要问题在于 insertSuffix(),也许是我对 unordered_map 结构缺乏了解和理解。任何人都可以对此有所了解吗?我是否在C ++变体中犯了一些新错误导致对象构造花了这么长时间?

有条件的信息

我已经编译了C#和C ++程序以获得最大速度/ O2(发布)

1 个答案:

答案 0 :(得分:6)

在C#中,System.String包含其Length,因此您可以在恒定时间内获得长度。在C ++中,std::string还包含其size,因此它也可以在固定时间内使用。

但是,您没有使用C ++ std::string(对于算法的良好翻译,您应该使用它);你正在使用C-style null-terminated char arraychar*字面意思是“指向char的指针”,只是告诉你字符串的第一个字符在哪里。 strlen函数查看指向前向的每个char,直到找到空字符'\0'(不要与null pointer混淆);这很昂贵,你可以在insertSuffix的循环的每次迭代中完成。这可能至少占你减速的一小部分。

在进行C ++时,如果你发现自己使用原始指针(任何涉及*的类型),你应该总是想知道是否有更简单的方法。有时答案是“不”,但通常是“是”(随着语言的发展,这种情况越来越普遍)。例如,考虑一下您的struct nodenode* root。两者都使用node指针,但在这两种情况下你都应该直接使用node,因为不需要那个间接(在node的情况下,一些间接量是必要的,因此您没有每个节点包含另一个节点 ad infinitum ,但这是由std::unordered_map提供的。

其他一些提示:

  • 在C ++中,您通常不希望在构造函数的主体中执行任何工作,而是使用initialization lists
  • 如果您不想复制作为参数传递的内容,则应将参数作为参考;而不是更改insertSuffixstd::string作为第一个参数,而是std::string const&;同样,contains应该采用std::string const&。更好的是,由于insertSuffix可以看到text成员,因此根本不需要使用第一个参数,只需使用from
  • C ++支持foreach-like construct,在迭代字符串的字符时,您可能更喜欢标准的for循环。
  • 如果你正在使用最新的非技术上最终但足够接近的C ++版本C ++ 17,那么只要你想要一个,你就应该使用std::string_view代替std::string查看一个字符串,不需要更改它或保持对它的引用。这对contains很有用,因为你想在text成员中创建一个本地副本,即使对于构造函数也是如此;它在text成员本身中没用,因为被查看的对象可能是临时的。但是,在C ++中,有时候生命周期很棘手,直到你掌握它为止,你可能只想使用std::string来保证安全。
  • 由于nodesuffixTree的概念之外没有用处,所以它应该在它内部,就像在C#版本中一样。作为对C#版本的偏离,您可能希望将类型node和数据成员roottext变为private而不是public成员。