Reputation: 1331
I am much more familiar with C# than C++ so I must ask for advice on this issue. I had to rewrite some code pieces to C++ and then (surprisingly) ran into performance issues.
I've narrowed the problem down to these snippets:
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];
}
}
}
In both cases I'm creating a suffix tree and later using it in a much bigger function which is not relevant for the post (lets call it F()). I've tested both on two randomly generated strings of length 100000. The C# version constructed my suffix tree and used it in F() in a total execution time of: 480 ms while the code which I've "translated to C++" executed in 48 sec
I've drilled this down further and it seems that in my C++ code, the constructor takes 47 sec while using the tree in F() runs at 48 ms which is 10 times faster than in C#.
Conclusion
It seems that the main problem is in insertSuffix(), perhaps my lack of knowledge and understanding of the unordered_map structure. Can anyone shed a light on this? Did I make some rookie mistake in the C++ variant which causes the object construction to take so long?
Aditional Info
I've compiled both the C# and C++ program for maximum speed /O2 (release)
Upvotes: 0
Views: 170
Reputation: 7443
In C#, a System.String includes its Length, so you can get the length in constant time. In C++, a std::string
also includes its size, so it is also available in constant time.
However, you aren’t using C++ std::string
(which you should be, for a good translation of the algorithm); you’re using a C-style null-terminated char
array. That char*
literally means “pointer to char
”, and just tells you where the first character of the string is. The strlen
function looks at each char
from the one pointed to forward, until it finds a null character '\0'
(not to be confused with a null pointer); this is expensive, and you do it in each iteration of your loop in insertSuffix
. That probably accounts for at least a reasonable fraction of your slowdown.
When doing C++, if you find yourself working with raw pointers (any type involving a *
), you should always wonder if there’s a simpler way. Sometimes the answer is “no”, but often it’s “yes” (and that’s getting more common as the language evolves). For example, consider your struct node
and node* root
. Both use node
pointers, but in both cases you should have used node
directly because there is no need to have that indirection (in the case of node
, some amount of indirection is necessary so you don’t have each node containing another node ad infinitum, but that’s provided by the std::unordered_map
).
A couple other tips:
insertSuffix
to take a std::string
as the first parameter, make it take std::string const&
; similarly, contains
should take a std::string const&
. Better yet, since insertSuffix
can see the text
member, it doesn’t need to take that first parameter at all and can just use from
.for
loop when iterating over a string’s characters.std::string_view
instead of std::string
whenever you just want a look at a string, and don’t need to change it or keep a reference to it around. This would be useful for contains
, and since you want to make a local copy in the text
member, even for the constructor; it would not be useful in the text
member itself, because the object being viewed might be temporary. Lifetime can sometimes be tricky in C++, though, and until you get the hang of it you might just want to use std::string
to be on the safe side.node
isn’t useful outside of the concept of suffixTree
, it should probably be inside it, like in the C# version. As a deviation from the C# version, you might want to make the type node
and the data members root
and text
into private
instead of public
members.Upvotes: 6