StringBuilder с кэшированием, ThreadStatic
Я наткнулся на приведенный ниже код из " Написание больших адаптивных приложений.NET Framework".
Код ниже создает строку вроде SomeType<T1, T2, T3>
с помощью StringBuilder
и демонстрация кеширования StringBuilder
улучшить производительность.
public void Test3()
{
Console.WriteLine(GenerateFullTypeName("SomeType", 3));
}
// Constructs a name like "SomeType<T1, T2, T3>"
public string GenerateFullTypeName(string name, int arity)
{
//StringBuilder sb = new StringBuilder();
StringBuilder sb = AcquireBuilder();
sb.Append(name);
if (arity != 0)
{
sb.Append("<");
for (int i = 1; i < arity; i++)
{
sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
}
sb.Append("T"); sb.Append(arity.ToString()); sb.Append(">");
}
//return sb.ToString();
/* Use sb as before */
return GetStringAndReleaseBuilder(sb);
}
[ThreadStatic]
private static StringBuilder cachedStringBuilder;
private static StringBuilder AcquireBuilder()
{
StringBuilder result = cachedStringBuilder;
if (result == null)
{
return new StringBuilder();
}
result.Clear();
cachedStringBuilder = null;
return result;
}
private static string GetStringAndReleaseBuilder(StringBuilder sb)
{
string result = sb.ToString();
cachedStringBuilder = sb;
return result;
}
Тем не менее, верно ли, что два измененных метода ниже лучше с точки зрения кэширования StringBuilder?? Только AcquireBuilder должен знать, как его кешировать.
private static StringBuilder AcquireBuilder()
{
StringBuilder result = cachedStringBuilder;
if (result == null)
{
//unlike the method above, assign it to the cache
cachedStringBuilder = result = new StringBuilder();
return result;
}
result.Clear();
//no need to null it
// cachedStringBuilder = null;
return result;
}
private static string GetStringAndReleaseBuilder(StringBuilder sb)
{
string result = sb.ToString();
//other method does not to assign it again.
//cachedStringBuilder = sb;
return result;
}
Другая проблема заключается в том, что исходные методы не являются потокобезопасными, почему ThreadStatic используется в демоверсии?
3 ответа
Основное внимание уделяется линии, которая создает новый экземпляр StringBuilder. Код вызывает выделение для sb.ToString() и внутренних выделений в реализации StringBuilder, но вы не можете управлять этими выделениями, если хотите получить строковый результат.
Ну, согласно примеру, они игнорировали свои слова. Лучше было бы просто кэшировать его и использовать повторно (очистить перед использованием). Нет выделений кроме тех что нужно:
public static string GenerateFullTypeName(string name, int arity)
{
//StringBuilder sb = new StringBuilder();
StringBuilder sb = cached.Value;
sb.Clear();
sb.Append(name);
if (arity != 0)
{
sb.Append("<");
for (int i = 1; i < arity; i++)
{
sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
}
sb.Append("T"); sb.Append(arity.ToString()); sb.Append(">");
}
//return sb.ToString();
/* Use sb as before */
return sb.ToString();
}
[ThreadStatic]
private static Lazy<StringBuilder> cached = new Lazy<StringBuilder>(()=> new StringBuilder());
Кроме того, я думаю, что это очень плохой пример того, как GC может повредить производительности вашего приложения. Временная и короткая струны практически никогда не входят во 2-е поколение и будут утилизированы в ближайшее время. Лучше было бы что-то вроде буферов для потоков передачи WCF, с возвратом этого буфера в пул, или как Task работает в целом (то же самое) и распределяет их кишечник, но не StringBuilder, duh.
Вот ответ на "оригинальные методы не являются потокобезопасными"
По сути, автор сделал - помеченное свойство с ThreadStaticAttribute
, что делает его потокобезопасным, потому что значение будет доступно только для этого потока. Разные темы будут иметь разные значения / ссылки. И этот "кеш" будет жить только на протяжении жизни самого потока. Даже если сам метод не является потокобезопасным, он получает доступ к значению.
Теперь, я не думаю, что это вообще отличный пример, потому что в этом смысл? Вы держите в стороне экземпляр "Жала", который вы все равно очищаете.
Если ваш конкретный интерес к каждому потоку статическое значение, ThreadStaticAttribute
это приятно иметь. Если вас больше интересуют потоковые статические методы, посмотрите на lock
private static MyClass _myClass;
private static object _lock = new object();
public static MyClass GetMyClass()
{
if (_myClass == null)
{
lock(_lock)
{
if (_myClass == null)
{
_myClass = new MyClass();
}
}
}
return _myClass;
}
Этот пример показывает только основную идею и не погружается слишком глубоко. Давайте расширим наш класс новыми методами для добавления пространства имен.
public string GenerateFullTypeName(string name, int arity, string @namespace)
{
StringBuilder sb = AcquireBuilder();
sb.Append(this.GenerateNamespace(@namespace));
sb.Append(this.GenerateFullTypeName(name, arity));
return GetStringAndReleaseBuilder(sb);
}
public string GenerateNamespace(string @namespace)
{
StringBuilder sb = AcquireBuilder();
sb.Append(@namespace);
sb.Append(".");
return GetStringAndReleaseBuilder(sb);
}
И проверить это Console.WriteLine(test.GenerateFullTypeName("SomeType", 3, "SomeNamespace"));
Исходный код работает как положено (выходная строка SomeNamespace.SomeType<T1, T2, T3>
), но что будет, если мы применим вашу "оптимизацию"? Выходная строка будет неправильной (SomeType<T1, T2, T3>SomeType<T1, T2, T3>
), потому что мы будем использовать только один (обналиченный) экземпляр StringBuilder
для всех методов в этом классе, даже этот экземпляр все еще используется. Вот почему экземпляр сохраняется в поле только после использования и удаляется из поля, если он снова используется.