Совет по недопустимой работе указателя при использовании сложных записей

Env: Delphi 2007

‹Обоснование> Я часто использую сложные записи, так как они предлагают почти все преимущества классов, но с гораздо более простой обработкой. ‹/Justification>

Как бы то ни было, одна особенно сложная запись, которую я только что реализовал, - это мусор памяти (позже приводящий к ошибке «Недопустимая операция указателя»).

Это пример кода очистки памяти:

sSignature := gProfiles.Profile[_stPrimary].Signature.Formatted(True);

Во второй раз, когда я называю это, я получаю "Недопустимая операция указателя"

Он работает нормально, если я назову это так:

  AProfile    := gProfiles.Profile[_stPrimary];
  ASignature  := AProfile.Signature;
  sSignature  := ASignature.Formatted(True);

Фоновый код:

  gProfiles: TProfiles;

  TProfiles = Record
  private
    FPrimaryProfileID: Integer;
    FCachedProfile: TProfile;
    ...
  public
    < much code removed >

    property Profile[ProfileType: TProfileType]: TProfile Read GetProfile;
  end;


  function TProfiles.GetProfile(ProfileType: TProfileType): TProfile;
  begin        
    case ProfileType of
      _stPrimary        : Result := ProfileByID(FPrimaryProfileID);
      ...
    end;
  end;

  function TProfiles.ProfileByID(iID: Integer): TProfile;
  begin
    <snip>
    if LoadProfileOfID(iID, FCachedProfile)  then
    begin
      Result := FCachedProfile;
    end
    else
    ...
  end;


  TProfile = Record
  private     
    ...
  public
    ...
    Signature: TSignature;
    ...
  end;


  TSignature = Record
  private               
  public
    PlainTextFormat : string;
    HTMLFormat      : string;

    // The text to insert into a message when using this profile
    function Formatted(bHTML: boolean): string;
  end;

  function TSignature.Formatted(bHTML: boolean): string;
  begin
    if bHTML then
      result := HTMLFormat
    else
      result := PlainTextFormat;
    < SNIP MUCH CODE >
  end;

Итак, у меня есть запись внутри записи внутри записи, которая приближается к путанице на начальном уровне, и я первым признаю, что это не совсем хорошая модель. Ясно, что мне придется его реструктурировать. Я хотел бы, чтобы вы, гуру, лучше понимали, почему он уничтожает память (что-то связанное со строковым объектом, который создается, а затем освобождается ...), чтобы я мог избежать подобных ошибок в будущем.

Спасибо


person Zax    schedule 22.12.2010    source источник
comment
Свойство типа запись? Я почти уверен, что это вообще не должно компилироваться. Недопустимая операция указателя означает, что вы освобождаете то, что уже было освобождено, или освобождаете то, что никогда не относилось к динамически выделяемой памяти.   -  person Rob Kennedy    schedule 22.12.2010
comment
@ Роб: почему это не должно компилироваться? Свойства записи разрешены, несмотря на то, что присвоение членам записи невозможно (`Obj.Record.Member: = 'blub')   -  person jpfollenius    schedule 22.12.2010
comment
Свойства записи +1 вполне разумны, например, как насчет записи комплексного числа, в которой используется перегрузка оператора?   -  person David Heffernan    schedule 22.12.2010


Ответы (3)


Ваше оправдание использования записей вместо классов кажется ошибочным. Каждый раз, когда вы возвращаете запись как результат функции или передаете запись как параметр функции или назначаете одну запись var другой записи, все поля этой структуры записи копируются в память.

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

Разница между последовательным вызовом трех функций в одном операторе и вызовом трех функций в отдельных операторах заключается в выделении и времени жизни промежуточных результатов. Когда вы вызываете функции в отдельных операторах, вы предоставляете локальные переменные для хранения промежуточных результатов между вызовами. Переменные указаны явно, и время их жизни четко определено.

Когда вы вызываете функции в одном операторе, компилятор отвечает за выделение временных переменных для хранения промежуточных результатов между вызовами. Анализ этих неявных переменных на протяжении жизни может стать непонятным - можно ли использовать одну и ту же локальную переменную для хранения промежуточных результатов нескольких последовательных вызовов? В большинстве случаев ответ, вероятно, положительный, но если задействованные типы записей содержат поля типов данных, управляемых компилятором (строки, варианты и интерфейсы), одна и та же локальная переменная не может быть просто перезаписана следующим блоком данных.

Записи, содержащие типы, управляемые компилятором, должны удаляться упорядоченным образом, чтобы избежать утечки памяти кучи. Если такая запись перезаписывается данными мусора или если такая запись копируется без ведома компилятора, то сгенерированный компилятором код для удаления управляемых компилятором полей записи, когда запись выходит за пределы области видимости, скорее всего, сообщит, что это обнаружил недопустимый указатель и / или поврежденную кучу.

Ваша запись TSignature содержит строковые поля, что делает ее типом данных, управляемым компилятором. Везде, где у вас есть локальная переменная типа TSignature, компилятор должен неявно сгенерировать try..finally кадры в теле функции, чтобы убедиться, что строковые поля в этой структуре локальной переменной освобождаются, когда выполнение выходит из этой области.

Любая операция, завершающаяся изменением или перезаписью указателей строковых полей в записи TSignature, может привести к ошибке Invalid Pointer Operation. Создание копий записи (путем присвоения ее нескольким переменным) должно автоматически увеличивать счетчики ссылок, но любое использование MemCopy для массового копирования содержимого записи в какое-либо другое место приведет к сбрасыванию счетчиков ссылок и приведет к недопустимой операции указателя при очистке код пытается освободить эти строковые поля больше раз, чем на них фактически ссылались. Приведение типа переменной записи к неправильному типу записи может привести к перезаписи строковых полей мусором и вызвать недопустимую операцию указателя в строке (когда запись очищается в конце области видимости)

Также существует вероятность того, что сам компилятор потерял отслеживание переменных промежуточной записи в сценарии с одним оператором и слишком много раз очищает скрытые промежуточные элементы или перезаписывает их, не очищая предыдущие значения. Где-то еще в эпоху Delphi 3 в этой области была ошибка компилятора, но я не помню, в каком выпуске продукта мы ее исправили. Кажется, я припоминаю ошибку, которую я имел в виду, когда результаты функции типа записи передавались в тип const параметры, поэтому это не точное совпадение с вашим сценарием, но последствия аналогичны.

Прежде чем сообщать об этом как об ошибке компилятора, просмотрите свой код с помощью зубчатой ​​гребешки в представлении разборки отладчика. Есть масса способов сделать все самостоятельно. Посмотрите, где промежуточные результаты распределяются, записываются и удаляются с помощью кода, созданного компилятором, и как ваш код взаимодействует с этим. шаблон.

Дымящийся пистолет будет, когда вы увидите, что строковые поля одной переменной временной записи перезаписываются без вызова для уменьшения ссылки на эти строки. Это может быть вызвано вашим кодом или что-то в коде, сгенерированном компилятором, но единственный способ узнать это - это засвидетельствовать действие и определить направление пальца оттуда.

person dthorpe    schedule 22.12.2010
comment
Спасибо за подробности (прочитал уже трижды). Я лично не беспокоюсь о том, чтобы указывать пальцами; только нахождение решения. Что бы вы предложили в качестве более безопасной модели для большого класса, требующего множества подклассов? - person Zax; 22.12.2010
comment
+1 (+10, если можно) за то, что нашел время так подробно объяснить. - person Marjan Venema; 22.12.2010
comment
@Xaz: По какой причине вы не можете использовать класс для моделирования класса? Я не могу придумать более простого обращения. Это управление памятью (на самом деле не так уж сложно, если следовать четкому принципу владения) - person jpfollenius; 22.12.2010
comment
-1 Компилятор Delphi делает действительно последовательную работу по обработке записей и управлению ссылочными типами в записях. С точки зрения RTL, теперь существует разница между типами с подсчетом ссылок в классах и в записях: вызываются одни и те же низкоуровневые функции system.pas. Ошибка, о которой вы говорите, возможно, связана с многопоточной ошибкой невыпущенных свойств строки (заблокированный DEC был введен в _LStrClr): но это затрагивало как классы, так и записи. Те же проблемы с использованием MemCopy / Move появятся как для записей, так и для классов. - person Arnaud Bouchez; 22.12.2010
comment
@ A.Bouchez Надеюсь, вы знаете, что dthorpe вела блог на странице ядра компилятора Delphi (web.archive. org / web * / blogs.borland.com/dcc); его глубокое знание компилятора Delphi было важной частью его работы. - person Jeroen Wiert Pluimers; 22.12.2010
comment
@ A.Bouchez MemCopy / Move используется гораздо чаще для записей, чем для классов, особенно потому, что люди думают, что им это сойдет с рук (они могут только для записей, не управляемых компилятором, но часто они понимают это слишком поздно или вообще не понимают) ). Одно это могло вызвать эту проблему. - person Jeroen Wiert Pluimers; 22.12.2010
comment
@ A.Bouchez Подсчет ссылок может вызвать хаос (мы вскоре напишем об этом в блоге) при передаче данных в константных параметрах; если у вас нет локальных ссылок на промежуточные результаты, это могло вызвать проблему. - person Jeroen Wiert Pluimers; 22.12.2010
comment
@ A.Bouchez вместо записи в блоге я разместил вопрос о проблеме подсчета ссылок: stackoverflow.com/questions/4509015/ - person Jeroen Wiert Pluimers; 22.12.2010
comment
@ A.Bouchez: Да, мне известно о заблокированном Dec в _LStrClr. Я это реализовал. ; › - person dthorpe; 22.12.2010
comment
@ A.Bouchez: Хотя вы правы, что нет существенной разницы с RTL, используете ли вы управляемые компилятором типы в качестве полей в записях или типов классов, существует огромная разница в том, как пользователь / разработчик использует типы записей по сравнению с классом. типы. Поскольку компилятор волшебным образом заботится о распределении типов записей и копировании значений между ними, разработчик с большей вероятностью попадет в ловушку игнорирования размера кода и затрат на производительность записей, особенно записей, содержащих поля, управляемые компилятором. (продолжение) - person dthorpe; 22.12.2010
comment
(... продолжение) Поскольку экземпляры типов классов передаются по ссылке и назначаются по ссылке, фактические данные экземпляра выделяются и освобождаются относительно редко по сравнению с копированием данных записи. Добавление локальной переменной типа класса в процедуру не приведет к добавлению в эту процедуру какого-либо дополнительного кода; добавление локальной переменной типа записи, которая содержит управляемые компилятором поля, добавит скрытый блок try..finally вокруг тела процедуры для очистки управляемых компилятором полей в этой записи. (продолжение) - person dthorpe; 22.12.2010
comment
(... продолжение) Эквивалентный код для очистки полей, управляемых компилятором, в типе класса находится в деструкторе класса. Разница в том, что деструктор класса вызывается только один раз для каждого экземпляра, тогда как код очистки записи вызывается в каждом теле процедуры, которое содержит переменную этого типа записи. Если вы часто используете записи, содержащие поля, управляемые компилятором, это может очень быстро привести к незаметной потере производительности. - person dthorpe; 22.12.2010
comment
@Xaz: нет ничего плохого в использовании записей для небольших значений или для фрагментов данных, которые не передаются часто, но для общих операций рекомендуется использовать тип класса для классов вместо записей. Именно поэтому мы в первую очередь создали тип класса. ; ›Чтобы управлять очисткой памяти классов, вам нужно подумать о времени жизни объектов и о том, кто должен быть владельцем каждого объекта. Посмотрите на VCL для руководства - модель владения компонентами VCL существует в первую очередь для обеспечения автоматического управления памятью для компонентов VCL - без накладных расходов на сборку мусора. - person dthorpe; 22.12.2010
comment
@ A.Bouchez: Я серьезно сомневаюсь, что Xaz запускает здесь несколько потоков. Недопустимая операция указателя в этом сценарии почти наверняка вызвана тем, что что-то перезаписывает значение промежуточной записи, используемое между вызовами функции с одним оператором, и удаляет поля указателя строки в записи подписи. - person dthorpe; 22.12.2010
comment
Что ж, вызвав такие оживленные дискуссии, я считаю, что моя работа здесь сделана (уходит в закат). А если серьезно, спасибо за отзывы. - person Zax; 30.12.2010
comment
@dthorpe Спасибо за все эти комментарии и обмен знаниями! Записи могут быть злыми (медленная скрытая копия, запись скрытого кода медленной очистки, fillchar сделает любую строку в записи утечкой памяти ...), но иногда они очень удобны для доступа к двоичной структуре (небольшое значение, как вы написали ) через указатель. Но используя указатели, вы должны знать, что делаете. Определенно предпочтительнее придерживаться классов и TPersistent, если вы хотите использовать волшебную модель владения компонентами VCL. Я пытаюсь использовать записи только в том случае, если в них нет полей с подсчетом ссылок. - person Arnaud Bouchez; 18.01.2011

Из предоставленного вами кода не видно, где происходит повреждение, поэтому вот несколько предложений. Попробуйте разные комбинации объединения полей, чтобы увидеть, сможете ли вы воспроизвести это.

AProfile := gProfiles.Profile[_stPrimary];
sSignature := AProfile.Signature.Formatted(True);

ASignature := gProfiles.Profile[_stPrimary].Signature;
sSignature := ASignature.Formatted(True);

Включите проверку диапазона и проверку переполнения, если вы еще этого не сделали. Загрузите FastMM4 и используйте его FullDebugMode. Если ничего из этого не приводит к ответу, узнайте, как использовать точки останова памяти.

person Kenneth Cochran    schedule 22.12.2010
comment
Оба эти метода работают без искажений. Похоже, что gProfiles.Profile [_stPrimary] .Signature освобождается после - person Zax; 22.12.2010
comment
Оба эти метода работают без искажений. Кажется, что gProfiles.Profile [_stPrimary] .Signature освобождается, когда метод завершается после первого вызова. FullDebugMode не вернул ничего, что могло бы мне помочь. - person Zax; 22.12.2010
comment
В частности, gProfiles.FCachedProfile.Signature освобождается после выхода из метода - person Zax; 22.12.2010
comment
Я думаю, вы здесь что-то упускаете. Использование метода, возвращающего TProfile, сделает локальную копию содержимого в локальную переменную AProfile. Тогда sSignature можно будет безопасно получить из AProfile. - person Arnaud Bouchez; 22.12.2010

Есть кое-что, что я не очень хорошо понимаю с извлечением вашего кода. TProfile - это рекорд? Таким образом, использование функции SomeName: TProfile сделает копию содержимого записи в результате, что очень неэффективно. Даже с оптимизированной версией функции копирования записей это все равно занимает много времени.

Вы должны получить его по ссылке / указателю, используя тип PProfile = ^ TProfile. В этом случае вы предотвратите большинство проблем с памятью, связанных с доступом к строке внутри записи.

Но вы должны быть уверены, что ваш исходный TProfile останется доступным в памяти в течение всего времени существования указателя PProfile.

Использование записей может быть быстрее / проще, чем использование классов в некоторых (редких) случаях, , например, если вы анализируете двоичный контент. Но вы никогда не должны использовать простой тип записи для управления записями с помощью функций / методов, а только указатель на запись (или параметр var). Так будет и безопаснее, и быстрее.

person Arnaud Bouchez    schedule 22.12.2010
comment
@MisterDownVoter, пожалуйста, оставьте комментарий, чтобы поделиться своим мнением! :) - person Arnaud Bouchez; 20.01.2011