我的问题很简单:在尝试进行转换时,我有一个真实的(未想象的)性能错误
"1273 912 84" --> {1273, 912, 84}
所以我想弄清楚如何尽可能快地完成它。
总有3个数字,前2个总是在[1,3000]范围内,最后2个总是在[1,100000]范围内。
背景:我正在研究HackerRank问题,我的解决方案是在其中一个测试用例上超时。我阅读了关于这个问题的讨论,一位老兄说,每个人都失败了最后一个测试用例的原因是因为他们解析了巨大的输入。
我尝试解决此性能错误的方法是创建类似
的类class IntParser
{
private Dictionary<string, int> _Map = new Dictionary<string, int>();
public IntParser(int n)
{
for (int i = 1; i <= n; ++i)
{
_Map[i.ToString()] = i;
}
}
public int[] ParseVals(string line)
{
return Array.ConvertAll(line.Split(' '), ParseVal);
}
public int ParseVal(string s)
{
int retval;
try
{
retval = _Map[s];
}
catch(KeyNotFoundException)
{
retval = Int32.Parse(s);
_Map[s] = retval;
}
return retval;
}
}
并使用
初始化它var parser = new IntParser(100000);
然后像
一样使用它int[] triplet = parser.ParseVals(Console.ReadLine());
令人惊讶的是,这仍然不够有效。
答案 0 :(得分:2)
您可以在O(N)时间和O(N)空间中执行此操作。 你只需要创建一个迭代你的角色并实现一个小状态机的函数。你可以只有3个州:
1)我正在读一个空间 2)我正在读一个数字 3)到达列表的末尾
可能的转变是: 1)直接读取一个数字 1)从阅读空间到阅读数字 2)从阅读数字到阅读空间或到达列表末尾
您只需调整代码并记住进入状态1)。每当您退出州1)时,您必须添加一个您已阅读的数字列表。
答案 1 :(得分:2)
如果不进行剖析,很难说清楚,但要记住几点:
String.Split
生成垃圾。TryGetValue
用于缓存。Int32.Parse
更快地解析整数。然而,“3,000,000多行”对我来说似乎不是一个大问题,所以我不确定解析是否是你真正的罪魁祸首。
为什么不配置文件? Visual Studio 2015社区捆绑了一个CPU分析器。
(更新)
我尝试通过完全删除String.Substring
并在同一方法中进行所有解析来改进@ Jamiec的答案,并提出:
static public int[] ParserInPlace(string s)
{
var result = new int[3];
var x = 0;
for (var i = 0; i < s.Length; i++)
{
if (s[i] == ' ')
{
x++;
continue;
}
result[x] = result[x] * 10 + (s[i] - '0');
}
return result;
}
在我的机器上,这似乎比@ Jamiec的随机数解决方案快2倍,比OP的原始代码快了~10倍(发布模式,x86,Works On My Machine™):
RANDOM INPUTS ParserBasic: 2068.4759ms OP's Parser: 1520.8422ms ParserNoSplit: 1300.3933ms ParserNoSplitNoIntParse: 322.271ms ParserInPlace: 125.0064ms SMALLEST INPUT (1 1 1) ParserBasic: 1715.9653ms OP's Parser: 702.8926ms ParserNoSplit: 1006.344ms ParserNoSplitNoIntParse: 203.4511ms ParserInPlace: 59.2876ms LARGEST INPUT (3000 3000 100000) ParserBasic: 1971.8206ms OP's Parser: 827.6612ms ParserNoSplit: 1256.9101ms ParserNoSplitNoIntParse: 274.5071ms ParserInPlace: 111.802ms
如果你需要一次解析一行,那么你可能不需要在每次迭代中分配相同的int[] result
数组(但你需要将它设置为零),这样你就可以减少垃圾甚至更进一步。
答案 2 :(得分:1)
我认为我能提出的最有效的方法就是不要使用String.Split
或int.Parse
。
我比较了4种不同的方式
String.Split
IntParser
这是我如何进行比较的。
我使用此algorythm创建了3,000,000个字符串以匹配您的输入
static string CreateExampleString()
{
return String.Join(" ", new[] { rnd.Next(1, 3000), rnd.Next(1, 3000), rnd.Next(1, 100000) });
}
我创建了一个基准测试程序
static TimeSpan Test(Func<string,int[]> parser, List<string> inputs)
{
Stopwatch sw = new Stopwatch();
sw.Start();
foreach (var input in inputs)
parser(input);
sw.Stop();
return sw.Elapsed;
}
然后我定义了4个测试用例,第一个是
static int[] ParserBasic(string input)
{
return input.Split(' ').Select(int.Parse).ToArray();
}
第二个就是我已经说过的IntParser
,第三个是
static public int[] ParserNoSplit(string s)
{
int[] result = new int[3];
int x = 0;
int b = 0;
for(var i=0;i<s.Length;i++)
{
if(s[i] == ' ')
{
result[x++] = int.Parse(s.Substring(b, i - b));
b = i + 1;
}
}
result[x] = int.Parse(s.Substring(b));
return result;
}
最后,我采用了上述内容并将int.Parse
替换为@CharlesMager答案中解析的代码,并获得了最佳结果!
static public int[] ParserNoSplitNoIntParse(string s)
{
int[] result = new int[3];
int x = 0;
int b = 0;
for (var i = 0; i < s.Length; i++)
{
if (s[i] == ' ')
{
result[x++] = ParseVal(s.Substring(b, i - b));
b = i + 1;
}
}
result[x] = ParseVal(s.Substring(b));
return result;
}
static int ParseVal(string s)
{
var result = 0;
for (var i = 0; i < s.Length; i++)
{
result = result * 10 + (s[i] - '0');
}
return result;
}
实际测试代码是
var allStrings = Enumerable.Range(0, 3000000).Select(x => CreateExampleString()).ToList();
var result1 = Test(ParserBasic, allStrings);
Console.WriteLine($"Result1: {result1.TotalMilliseconds}ms");
var result2 = Test(parser.ParseVals, allStrings);
Console.WriteLine($"Result2: {result2.TotalMilliseconds}ms");
var result3 = Test(ParserNoSplit, allStrings);
Console.WriteLine($"Result3: {result3.TotalMilliseconds}ms");
var result4 = Test(ParserNoSplitNoIntParse, allStrings);
Console.WriteLine($"Result4: {result4.TotalMilliseconds}ms");
通过每个输入运行相同的300万个输入的结果如下:
结果1:2491ms
结果2:2132ms
结果3:1579ms
结果4:861毫秒
哪个更好,当然。它可以在更多方面得到改进,但避免String.Split
可以提高整体性能。
答案 3 :(得分:0)
试试这个(一定要使用Try Catch,以防你的字符串里面没有可转换的字符串:
string myString = "1234 321 145";
int[] myInts = Array.ConvertAll(myString.Split(' '), int.Parse);
在这里编辑: 我做了一个测试,看看花了多少时间:
List<String> list = new List<String>();
Random rnd = new Random();
for (int i = 0; i < 3000; i++) {
list.Add(String.Join(" ", new[] { rnd.Next(1, 3000), rnd.Next(1, 3000), rnd.Next(1, 100000) }));
}
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 3000; i++) {
int[] myInts = Array.ConvertAll(list[i].Split(' '), int.Parse);
}
sw.Stop();
MessageBox.Show(sw.Elapsed.ToString());
结果是:00:00:00.0016167
答案 4 :(得分:0)
使用此实现,我的速度提高了43%(每个dotTrace):
public int ParseVal(string s)
{
var result = 0;
for (var i = 0; i < s.Length; i++)
{
result = result*10 + (s[i] - '0');
}
return result;
}
大部分时间(70%)现在都在String.Split
- 快速谷歌会提供可能有用的内容like this。