Использование интернирования строк для уменьшения использования памяти сетевого клиента

У меня есть сетевой клиент, который обрабатывает данные с сервера.

Данные отправляются в виде серии сообщений, которые сами по себе представляют собой наборы ключей / значений, по концепции сходные с заголовками HTTP (за исключением того, что нет "тела сообщения"), вот типичное одностороннее сообщение (строки, разделенные \r\n):

Response: OK
Channel: 123
Status: OK
Message: Spectrum is green
Author: Gerry Anderson
Foo123: Blargh

Мой клиент протокола работает, читая из NetworkStreamпосимвольно используя StreamReader а также while( (nc = rdr.Read()) != -1 ) и использует анализатор конечного автомата и StringBuilder экземпляр для заполнения Dictionary<String,String> экземпляров. Эти экземпляры словаря затем сохраняются в структурах в памяти для дальнейшей обработки, обычно они имеют полезную продолжительность жизни около 10 минут каждый.

Мой клиент получает тысячи таких сообщений в час, и процесс клиента длится долго - это проблема, потому что мой клиентский процесс часто растет, потребляя более 2 ГБ памяти, все из этих String случаи - я использовал windbg, чтобы увидеть, куда уходит вся память. Это проблема, потому что код выполняется на виртуальной машине Azure только с 3,5 ГБ памяти. Я не вижу причин, по которым моя программа должна потреблять не более нескольких сотен МБ ОЗУ. Часто я присматриваю за виртуальной машиной и наблюдаю за тем, как со временем расходуется память моего процесса, и она неуклонно возрастает до 2 ГБ, затем внезапно падает до 100 МБ, когда GC выполняет сбор данных, а затем снова растет. Время может варьироваться между запусками GC, без какой-либо предсказуемости.

Потому что многие из этих строк идентичны (например, ключи Response, Statusи т. д.), а также известные значения, такие как OK а также Fail Я могу использовать интернирование строк, чтобы уменьшить использование, например так:

// In the state-machine parser after having read a Key name:

String key = stringBuilder.ToString();
key = String.Intern( key );

// etc... after reading value
messageDictionary.Add( key, value );

Проблема в том, что я вижу место для дополнительной оптимизации: sb.ToString() собирается выделить новый экземпляр строки, который будет использоваться для интернирования, а во-вторых: интернированные строки сохраняются в течение всего срока службы домена приложения, и, к сожалению, некоторые ключи не будут повторно использоваться и фактически будут тратить память, например Foo123 в моем примере протокола.

Я решил, что одним из решений является не использовать интернирование строк, а вместо этого иметь класс, содержащий static readonly строковые поля, которые являются известными ключами, а затем используют обычные, не интернированные строки - которые в конечном итоге были бы собраны GC и поэтому не рискуют заполнить внутренний пул строк одноразовыми строками. Я бы тогда сравнил StringBuilder экземпляр этих известных строк, и если это так, используйте их вместо вызова sb.ToString() таким образом пропуская другое распределение строки.

Однако, если я выберу интернирование каждой строки, пул интернирования будет продолжать расти, и, к сожалению,.NET, похоже, не имеет .Chlorinate() метод для пула строк, есть ли способ удалить одноразовые строки из внутреннего пула, если я продолжу с String.Intern подход, или мне лучше использовать мои собственные статические экземпляры только для чтения строк?

1 ответ

Решение

Стажировка здесь не поможет по причинам, которые вы указали. Это на самом деле усугубит ситуацию, так как интернированные строки больше не подлежат сборке мусора. И нет, нет способа удалить интернированные строки из пула.

Вы описали, что GC делает именно то, для чего предназначен GC, поэтому мне не совсем ясно, что у вас действительно есть проблема. Принятие интернирования означало бы торговать сборщиком мусора (что не является проблемой) для постоянно растущего спроса на память (что является проблемой).

Если вы обеспокоены тем, что GC не запускается достаточно часто, чтобы поддерживать потребление памяти ниже некоторого порогового значения, которое вы имеете в виду, вы можете рассмотреть возможность мониторинга использования памяти и вызова GC.Collect(), когда вы достигнете этого порогового значения.

Если модель поведения GC на самом деле вызывает проблему (отличную от странной), вы можете попробовать переключиться из режима GC по умолчанию на "рабочей станции" в режим "сервера" GC, поскольку они настроены по-разному. (Но, опять же, я совсем не уверен, что у вас действительно есть проблема.)

Некоторые из различий описаны на этих двух страницах:

http://msdn.microsoft.com/en-us/library/ee787088(v=vs.110).aspx

http://blogs.msdn.com/b/dotnet/archive/2012/07/20/the-net-framework-4-5-includes-new-garbage-collector-enhancements-for-client-and-server-apps.aspx

Но обратите внимание, что фактические различия меняются с каждым выпуском фреймворка, потому что люди, ответственные за этот материал, постоянно учатся и делают улучшения.

И режим GC регулируется конфигурацией приложения:

http://msdn.microsoft.com/en-us/library/cc165011(v=office.11).aspx

<configuration
   <runtime>
      <gcServer enabled="true"/>
   </runtime>
</configuration> 

Вы также можете найти это руководство по устранению неполадок полезным или, по крайней мере, интересным:

http://msdn.microsoft.com/en-us/library/ee851764(v=vs.110).aspx

Другие вопросы по тегам