String.Format은 StringBuilder만큼 효율적인가요?
-
08-06-2019 - |
문제
C#에 다음과 같은 작업을 수행하는 stringbuilder가 있다고 가정해 보겠습니다.
StringBuilder sb = new StringBuilder();
string cat = "cat";
sb.Append("the ").Append(cat).(" in the hat");
string s = sb.ToString();
그것은 다음을 갖는 것만큼 효율적이거나 더 효율적일까요?
string cat = "cat";
string s = String.Format("The {0} in the hat", cat);
그렇다면 왜 그렇습니까?
편집하다
몇 가지 흥미로운 답변을 듣고 나서 나는 내가 질문한 내용을 좀 더 명확하게 설명했어야 했다는 것을 깨달았습니다.나는 문자열을 연결하는 데 어느 것이 더 빠른지, 어느 것이 더 빠른지 묻지 않았습니다. 주사 한 문자열을 다른 문자열로 연결합니다.
위의 두 경우 모두 미리 정의된 템플릿 문자열 중간에 하나 이상의 문자열을 삽입하고 싶습니다.
혼란을 드려 죄송합니다
해결책
메모: 이 답변은 .NET 2.0이 현재 버전일 때 작성되었습니다.이후 버전에는 더 이상 적용되지 않을 수 있습니다.
String.Format
사용하다 StringBuilder
내부적으로:
public static string Format(IFormatProvider provider, string format, params object[] args)
{
if ((format == null) || (args == null))
{
throw new ArgumentNullException((format == null) ? "format" : "args");
}
StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
builder.AppendFormat(provider, format, args);
return builder.ToString();
}
위 코드는 mscorlib의 일부이므로 질문은 "is"가 됩니다. StringBuilder.Append()
보다 빠른 StringBuilder.AppendFormat()
"?
벤치마킹이 없었다면 위의 코드 샘플은 다음을 사용하여 더 빠르게 실행될 것이라고 말할 수 있습니다. .Append()
.하지만 이는 추측일 뿐입니다. 적절한 비교를 위해 두 가지를 벤치마킹 및/또는 프로파일링해 보세요.
Jerry Dixon이라는 이 친구는 몇 가지 벤치마킹을 수행했습니다.
http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm
업데이트됨:
안타깝게도 위의 링크는 그 이후로 사라졌습니다.그러나 Way Back Machine에는 여전히 사본이 있습니다.
결국 문자열 형식이 반복적으로 호출되는지 여부에 따라 다릅니다.100MB가 넘는 텍스트를 처리하는 중인지, 사용자가 버튼을 클릭할 때 호출되는지 여부를 확인합니다.대규모 일괄 처리 작업을 수행하지 않는 한, 코드 가독성에 도움이 되는 String.Format을 사용하겠습니다.성능 병목 현상이 의심되는 경우 코드에 프로파일러를 적용하고 실제 위치를 확인하세요.
다른 팁
로부터 MSDN 문서:
String 또는 StringBuilder 개체에 대한 연결 작업의 성능은 메모리 할당이 발생하는 빈도에 따라 달라집니다.문자열 연결 작업은 항상 메모리를 할당하는 반면, StringBuilder 연결 작업은 StringBuilder 개체 버퍼가 너무 작아서 새 데이터를 수용할 수 없는 경우에만 메모리를 할당합니다.결과적으로, 고정된 수의 String 객체가 연결되는 경우 연결 작업에는 String 클래스가 더 좋습니다.이 경우 개별 연결 작업은 컴파일러에 의해 단일 작업으로 결합될 수도 있습니다.임의 개수의 문자열이 연결되는 경우 연결 작업에는 StringBuilder 개체가 더 좋습니다.예를 들어 루프가 사용자 입력 문자열을 무작위로 연결하는 경우입니다.
몇 가지 빠른 성능 벤치마크를 실행했는데 10회 실행에 대한 평균 100,000번의 작업에 대해 첫 번째 방법(문자열 빌더)은 두 번째 방법(문자열 형식)에 비해 거의 절반의 시간이 소요되었습니다.
따라서 이것이 자주 발생하지 않더라도 문제가 되지 않습니다.하지만 일반적인 작업이라면 첫 번째 방법을 사용하는 것이 좋습니다.
나는 기대할 것이다 문자열.형식 속도가 느려지려면 문자열을 구문 분석해야 하고 그 다음에 그것을 연결하십시오.
몇 가지 메모:
- 체재 전문 응용 프로그램에서 사용자가 볼 수 있는 문자열을 사용하는 방법입니다.이는 현지화 버그를 방지합니다.
- 결과 문자열의 길이를 미리 알고 있는 경우 다음을 사용하십시오. 스트링빌더(Int32) 용량을 미리 정의하는 생성자
string.Format이 여러분이 생각하는 것과 정확히 일치하지 않기 때문에 6년 후 Net45에서 테스트를 다시 실행해 보겠습니다.
Concat은 여전히 가장 빠르지만 실제로는 30% 미만의 차이가 있습니다.StringBuilder와 Format은 거의 5~10% 정도 다릅니다.몇 번 테스트를 실행하면서 20%의 변형을 얻었습니다.
밀리초, 백만 번의 반복:
- 연쇄:367
- 각 키에 대한 새로운 stringBuilder:452
- 캐시된 StringBuilder:419
- 문자열.형식:475
제가 얻은 교훈은 성능 차이가 사소하므로 읽을 수 있는 가장 간단한 코드를 작성하는 것을 중단해서는 안 된다는 것입니다.내 돈을 위해 자주 발생하지만 항상 그런 것은 아닙니다. a + b + c
.
const int iterations=1000000;
var keyprefix= this.GetType().FullName;
var maxkeylength=keyprefix + 1 + 1+ Math.Log10(iterations);
Console.WriteLine("KeyPrefix \"{0}\", Max Key Length {1}",keyprefix, maxkeylength);
var concatkeys= new string[iterations];
var stringbuilderkeys= new string[iterations];
var cachedsbkeys= new string[iterations];
var formatkeys= new string[iterations];
var stopwatch= new System.Diagnostics.Stopwatch();
Console.WriteLine("Concatenation:");
stopwatch.Start();
for(int i=0; i<iterations; i++){
var key1= keyprefix+":" + i.ToString();
concatkeys[i]=key1;
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
Console.WriteLine("New stringBuilder for each key:");
stopwatch.Restart();
for(int i=0; i<iterations; i++){
var key2= new StringBuilder(keyprefix).Append(":").Append(i.ToString()).ToString();
stringbuilderkeys[i]= key2;
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
Console.WriteLine("Cached StringBuilder:");
var cachedSB= new StringBuilder(maxkeylength);
stopwatch.Restart();
for(int i=0; i<iterations; i++){
var key2b= cachedSB.Clear().Append(keyprefix).Append(":").Append(i.ToString()).ToString();
cachedsbkeys[i]= key2b;
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
Console.WriteLine("string.Format");
stopwatch.Restart();
for(int i=0; i<iterations; i++){
var key3= string.Format("{0}:{1}", keyprefix,i.ToString());
formatkeys[i]= key3;
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
var referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway= concatkeys.Union(stringbuilderkeys).Union(cachedsbkeys).Union(formatkeys).LastOrDefault(x=>x[1]=='-');
Console.WriteLine(referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway);
대부분의 경우 효율성이 아닌 이러한 명확성이 가장 큰 관심사가 되어야 한다고 생각합니다.수많은 줄을 뭉개거나 저전력 모바일 장치용으로 무언가를 만드는 것이 아니라면 실행 속도에 큰 영향을 미치지 않을 것입니다.
나는 상당히 선형적인 방식으로 문자열을 작성하는 경우 직선 연결을 수행하거나 StringBuilder를 사용하는 것이 최선의 선택이라는 것을 발견했습니다.작성 중인 문자열의 대부분이 동적인 경우에 이 방법을 제안합니다.정적 텍스트는 거의 없기 때문에 가장 중요한 점은 향후 업데이트가 필요한 경우를 대비해 동적 텍스트의 각 부분이 어디에 배치되는지 명확하다는 것입니다.
반면에 2~3개의 변수가 포함된 큰 덩어리의 정적 텍스트에 대해 이야기하는 경우 효율성이 조금 떨어지더라도 string.Format을 통해 얻을 수 있는 명확성은 그만한 가치가 있다고 생각합니다.저는 이번 주 초에 4페이지짜리 문서 중앙에 동적 텍스트 한 비트를 배치해야 할 때 이것을 사용했습니다.세 부분을 연결하여 업데이트하는 것보다 큰 텍스트 덩어리를 한 부분으로 업데이트하는 것이 더 쉬울 것입니다.
String.Format은 다음을 사용합니다. StringBuilder
내부적으로...너무 논리적으로 이는 더 많은 오버헤드로 인해 성능이 약간 떨어질 것이라는 생각으로 이어집니다.그러나 간단한 문자열 연결은 다른 두 문자열 사이에 하나의 문자열을 삽입하는 가장 빠른 방법입니다.이 증거는 몇 년 전 Rico Mariani의 첫 번째 성능 퀴즈에서 입증되었습니다.간단한 사실은 연결이...문자열 부분의 수를 알고 있을 때(제한 없이...천 개의 부분을 연결할 수 있습니다...항상 1000개의 부분을 알고 있는 한)...항상 더 빠릅니다. StringBuilder
또는 문자열.형식.일련의 메모리 복사본을 단일 메모리 할당으로 수행할 수 있습니다. 여기 증거이다
다음은 포인터를 사용하여 메모리를 복사하는 FillStringChecked를 궁극적으로 호출하는 일부 String.Concat 메서드에 대한 실제 코드입니다(Reflector를 통해 추출됨).
public static string Concat(params string[] values)
{
int totalLength = 0;
if (values == null)
{
throw new ArgumentNullException("values");
}
string[] strArray = new string[values.Length];
for (int i = 0; i < values.Length; i++)
{
string str = values[i];
strArray[i] = (str == null) ? Empty : str;
totalLength += strArray[i].Length;
if (totalLength < 0)
{
throw new OutOfMemoryException();
}
}
return ConcatArray(strArray, totalLength);
}
public static string Concat(string str0, string str1, string str2, string str3)
{
if (((str0 == null) && (str1 == null)) && ((str2 == null) && (str3 == null)))
{
return Empty;
}
if (str0 == null)
{
str0 = Empty;
}
if (str1 == null)
{
str1 = Empty;
}
if (str2 == null)
{
str2 = Empty;
}
if (str3 == null)
{
str3 = Empty;
}
int length = ((str0.Length + str1.Length) + str2.Length) + str3.Length;
string dest = FastAllocateString(length);
FillStringChecked(dest, 0, str0);
FillStringChecked(dest, str0.Length, str1);
FillStringChecked(dest, str0.Length + str1.Length, str2);
FillStringChecked(dest, (str0.Length + str1.Length) + str2.Length, str3);
return dest;
}
private static string ConcatArray(string[] values, int totalLength)
{
string dest = FastAllocateString(totalLength);
int destPos = 0;
for (int i = 0; i < values.Length; i++)
{
FillStringChecked(dest, destPos, values[i]);
destPos += values[i].Length;
}
return dest;
}
private static unsafe void FillStringChecked(string dest, int destPos, string src)
{
int length = src.Length;
if (length > (dest.Length - destPos))
{
throw new IndexOutOfRangeException();
}
fixed (char* chRef = &dest.m_firstChar)
{
fixed (char* chRef2 = &src.m_firstChar)
{
wstrcpy(chRef + destPos, chRef2, length);
}
}
}
그럼:
string what = "cat";
string inthehat = "The " + what + " in the hat!";
즐기다!
아, 가장 빠른 방법은 다음과 같습니다.
string cat = "cat";
string s = "The " + cat + " in the hat";
그것은 정말로 달려있습니다.연결이 거의 없는 작은 문자열의 경우 실제로 문자열을 추가하는 것이 더 빠릅니다.
String s = "String A" + "String B";
그러나 더 큰 문자열(매우 큰 문자열)의 경우 StringBuilder를 사용하는 것이 더 효율적입니다.
위의 두 경우 모두 미리 정의된 템플릿 문자열 중간에 하나 이상의 문자열을 삽입하고 싶습니다.
어떤 경우에는 정확한 목적을 위한 디자인이므로 String.Format이 가장 빠르다고 제안합니다.
실제로는 사용 패턴에 따라 다릅니다.
사이의 상세한 벤치마크 string.Join
, string,Concat
그리고 string.Format
여기에서 찾을 수 있습니다: String.Format은 집중적 로깅에 적합하지 않습니다.
String.Format은 연결용으로 설계되지 않았으므로 날짜와 같은 다양한 입력의 출력 형식을 지정하기 위한 설계이므로 권장하지 않습니다.
String s = String.Format("Today is {0:dd-MMM-yyyy}.", DateTime.Today);