【问题标题】:Why does translating this code snippet from C# to C++ degrade performance?为什么将此代码片段从 C# 转换为 C++ 会降低性能?
【发布时间】:2017-08-18 07:40:48
【问题描述】:

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

我已将问题缩小到这些 sn-ps:

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];
        }
    }
}

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

内执行

我对此进行了进一步研究,似乎在我的 C++ 代码中,构造函数需要 47 秒,而在 F() 中使用树的运行时间为 48 毫秒这比 C# 快 10 倍。

结论

看来主要问题出在insertSuffix()上,可能是我对unordered_map结构缺乏了解和理解。任何人都可以对此有所了解吗?我是否在 C++ 变体中犯了一些菜鸟错误,导致对象构造需要这么长时间?

附加信息

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

【问题讨论】:

  • for (int i = from; i &lt; strlen(str); ++i) 这太贵了。在循环的每次迭代中,您都会重新计算 char* 字符串的长度。如果您使用 C++,请使用 std::string
  • 如果我之前将长度存储在变量中并继续使用char*可以吗?使用 std::string 还有其他好处吗?我会在稍后测试您的建议。
  • 使用char* 是一个非常糟糕的主意。使用std::string没有理由使用char* 而不是std::string
  • 贾斯汀 我可以确认您已经提供了解决方案。我想我已经看了太久了,并没有发现这个明显的错误嘿嘿。请善意将其发布为答案,以便我接受并投票。非常感谢!

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


【解决方案1】:

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

但是,您没有使用 C++ std::string(您应该使用 C++,以便更好地翻译算法);您使用的是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
  • 当你不想复制你作为参数传递的东西时,你应该让参数成为一个引用;而不是将insertSuffix 改为将std::string 作为第一个参数,而是将其改为std::string const&amp;;同样,contains 应该采用std::string const&amp;。更好的是,由于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 成员。

【讨论】:

  • 哇,我今天学到了很多,感谢您的详细阐述!我比 C++ 更接近 C 风格的语法。即使是我的 Intellisense 也很困惑,强调不存在的错误。
  • @Ilhan 为了有效地将其转换为 C 而不是 C++,您需要单独跟踪长度,这样您就不需要一直调用 strlen。我还在最后添加了一个关于将node 放入suffixTree 的提示; C++ 支持这一点,就像 C# 一样,它提供了相同的封装优势。
猜你喜欢
  • 2010-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多