|
08.01.2003, 13:27 | #1 |
Участник
|
Оптимизация класса Tax
Постановка задачи:
На основании модификации, полученной из офиса Колумбуса, изменить механизм расчета налогов при проведении накладной для повышения производительности. Модификация: В классе Tax изменен метод adjustAmount PHP код:
Действительно, при расчете налогов по многострочным заказам (закупкам) класс Tax делает очень много пересчетов суммы налогов. Особенно хорошо это видно, если на данную операцию натравить профайлер - Аксапта "уйдет в себя" на продолжительное время. Данная модификация должна решить эту проблему, но есть вопросы. Вопросы: 1. Какой выигрыш в производительности может дать такое изменение кода? 2. Как это скажется на потомках Tax-а и на расчете "withoutSource"? 1+2 Стоит ли игра свеч, а результат труда (с учетом анализа своих потомков Tax-а)? 3. Кто-дь использует у себя такую оптимизацию? 4. Если все хорошо, то почему ее нет в SP и HF? P.S. Не знаю как эта тема согласуется с авторскими правами. P.S.S. Сам еще на занимался плотным анализом механизмов класса Tax и потомков. Поэтому, будет интересна любая инфа по теме. |
|
20.11.2009, 12:45 | #2 |
Участник
|
А кто применял данный фикс на реальном проекте ?
Вопрос связан с тем, что сомнения одолели. Даже не верится, что такой маленький фикс решит такую большую проблему. Где-то должен быть подвох. |
|
03.05.2011, 13:46 | #3 |
Moderator
|
Ладно, поскольку дискуссия с Logger перешла в эту ветку, пожалуй продолжу здесь.
Во первых, в версии 2009 этот метод (adjustAmount) изрядно подоптимизировали. Во вторых - он ничего такого не делает Логика достаточно простая:
Однако есть одна загводка: Как известно, временные таблицы хранятся в памяти только если их размер не превышает 128 КБайт. Если размер превысил эту величину, то AOS выгружает их во временный файл, запросами и доступом к которому занимается сам AOS. А внутренний процессор запросов в AOS не быстрый и не оптимизировали его с прошлого столетия, вероятно он часто в fullscan скатывается Размер одной записи в tmpTaxWorkTrans в стандартной конфигурации, составляет 793 байта. Получается что где-то после 160 строк в заказе/закупке, данные из таблицы выгружаются на диск и начинают страшно тормозить в запросах. Если в новой версии (2012) tmpTaxWorkTrans переделали в SQL-ную временную таблицу, это должно заметно ускорить процесс. Также можно надеятся, что в 64битной версии сервера (а мне кажется 32битной и не будет) наконец-то отменили ограничение в 128 Кбайт под временную таблицу в памяти и теперь таблица будет храниться в памяти пока таковая имеется. Последний раз редактировалось fed; 03.05.2011 в 13:49. |
|
|
За это сообщение автора поблагодарили: Logger (5), ziva (2). |
29.11.2011, 21:25 | #4 |
Участник
|
Цитата:
Сообщение от fed
...
Однако есть одна загводка: Как известно, временные таблицы хранятся в памяти только если их размер не превышает 128 КБайт. Если размер превысил эту величину, то AOS выгружает их во временный файл, запросами и доступом к которому занимается сам AOS. А внутренний процессор запросов в AOS не быстрый и не оптимизировали его с прошлого столетия, вероятно он часто в fullscan скатывается Размер одной записи в tmpTaxWorkTrans в стандартной конфигурации, составляет 793 байта. Получается что где-то после 160 строк в заказе/закупке, данные из таблицы выгружаются на диск и начинают страшно тормозить в запросах. Если в новой версии (2012) tmpTaxWorkTrans переделали в SQL-ную временную таблицу, это должно заметно ускорить процесс. Также можно надеятся, что в 64битной версии сервера (а мне кажется 32битной и не будет) наконец-то отменили ограничение в 128 Кбайт под временную таблицу в памяти и теперь таблица будет храниться в памяти пока таковая имеется. Идея в том, чтобы заставить времянки создаваться в памяти. Например, можно завести виртуальный диск, реально живущий в памяти и прописать его в переменную окружения как темповую папку для пользователя, под которым работает сервер приложения. В таком случае файлы *.$$$, содержащие времянки будут реально жить в памяти и работать быстро. К сожалению, это не решило всех проблем. Если поиск во времянке делать по неиндексированному полю, то время улучшается примерно в 6-7 раз. Если по индексированному, то в 2 раза. (Т.е. индексы на больших времянках тоже существенно влияют на скорость) Также примерно в 2 раза ускоряется операция вставки. Мы ожидали бОльшего ускорения, поэтому на боевой базе не стали это применять. Тест проводили на примерно 100-200 тысячах записей буферов UnitConvert. Это составило примерно 50-100 мегабайт в $$$ файле на диске. |
|
|
За это сообщение автора поблагодарили: S.Kuskov (5). |
03.02.2022, 20:32 | #5 |
Участник
|
Я тут в очередной раз занимался оптимизацией разноски розничных продаж и наткнулся еще на пару возможных улучшений.
1. Метод writeTaxAmount_W, который тут ранее оптимизировали вызывает в цикле метод CustVendInvoiceTrans.initFromTaxWorkTrans_RU(). Там выполняется неиндексируемый запрос к злосчастной темповухе TmpTaxWorkTrans. Единственный селективный фильтр там это номер лота InventTransId, но индекса по нему нету. Получается примерно такая трассировка В общем нужно добавить индекс и плюс пришлось еще поменять немного код на поиске. X++: if (SysCountryRegionCode::isLegalEntityInCountryRegion([#isoRU])) { //+ sergey.m 03.02.2022 FRE_20421639_001 if (!_sourceRecId) { select taxWorkTrans index hint InventTransIdx where taxWorkTrans.SourceTableId == _sourceTableId && taxWorkTrans.InventTransId == _inventTransId && taxWorkTrans.TaxDirection != TaxDirection::UseTax && taxWorkTrans.TaxOrigin != TaxOrigin::TaxReversed; } else { //- sergey.m 03.02.2022 FRE_20421639_001 select taxWorkTrans where taxWorkTrans.SourceTableId == _sourceTableId && ((_sourceRecId && taxWorkTrans.SourceRecId == _sourceRecId) || (! _sourceRecId && taxWorkTrans.InventTransId == _inventTransId)) && taxWorkTrans.TaxDirection != TaxDirection::UseTax && taxWorkTrans.TaxOrigin != TaxOrigin::TaxReversed; } 2. В классе Tax метод lineTaxAmount. В начале метода проверяется что в таблице есть записи немного экзотическим методом, считая их количество. Я поменял так, хотя можно было наверное вообще убрать запрос, меня пока и так устраивает. X++: if (this.taxParameters().TaxSpecifyLine) { if (this.taxParameters().TaxSpecifyLine) { //+ sergey.m 03.02.2022 FRE_20421639_001 //select count(RecId) from taxWorkTrans; select firstOnly RecId from taxWorkTrans; //- sergey.m 03.02.2022 FRE_20421639_001 if (taxWorkTrans.RecId > 0 && !this.useSubLedgerJournalLines()) { // Posting out of TmpTaxWorkTrans |
|
|
За это сообщение автора поблагодарили: trud (10), Logger (5). |
03.02.2022, 21:03 | #6 |
Участник
|
Цитата:
Я для 2009-й кое что правил - некие частные случаи с накладными расходами. Там проблема из-за нелинейной зависимости от числа строк при разноске. (где то на форуме была тема) Где возможно - 1. отключали корреспонденцию. 2. для больших документов (более 300 строк) вместо одной накладной делали кучку накладных по 40 строк. тем самым линеаризовали зависимость времени разноски от числа строк. - у нас были компании для управленческого учета где число накладных было неважно, главное итоговые показатели. Вот там такое было применимо. |
|
03.02.2022, 21:22 | #7 |
Участник
|
|
|
04.02.2022, 08:51 | #8 |
Участник
|
Цитата:
Сообщение от Masel
Я тут в очередной раз занимался оптимизацией разноски розничных продаж и наткнулся еще на пару возможных улучшений.
1. Метод writeTaxAmount_W, который тут ранее оптимизировали вызывает в цикле метод CustVendInvoiceTrans.initFromTaxWorkTrans_RU(). Там выполняется неиндексируемый запрос к злосчастной темповухе TmpTaxWorkTrans. Единственный селективный фильтр там это номер лота InventTransId, но индекса по нему нету. Получается примерно такая трассировка - на одну строку накладной одна запись в TmpTaxWorkTrans для RU, связь по номеру лота (по логике можно хранить и несколько записей, но кажется в этом случае особого профита по сравнению с индексом не будет) - получается можно до цикла по строкам получить map - ключ inventTransId, значение запись TmpTaxWorkTrans(в методе который собственно и формирует всю TmpTaxWorkTrans, т.е. дополнительных проходов не потребуется). - в цикле из мапа получать запись TmpTaxWorkTrans и ее отдавать в метод уже (в самом методе и чуть ниже придется поменять код, так что обрабатывать запись, а не курсор, но это не кажется сложным).
__________________
Sergey Nefedov |
|
04.02.2022, 11:28 | #9 |
Участник
|
Цитата:
Сообщение от SRF
На уровне идеи (не факт конечно, что будет быстрее) - можно же вообще убрать запрос в этом месте к темповой табличке для RU функциональности(наверное и в общем случае можно, но будет чуть сложнее), я может конечно что то забываю или не учитываю, но идея такая :
- на одну строку накладной одна запись в TmpTaxWorkTrans для RU, связь по номеру лота (по логике можно хранить и несколько записей, но кажется в этом случае особого профита по сравнению с индексом не будет) - получается можно до цикла по строкам получить map - ключ inventTransId, значение запись TmpTaxWorkTrans(в методе который собственно и формирует всю TmpTaxWorkTrans, т.е. дополнительных проходов не потребуется). - в цикле из мапа получать запись TmpTaxWorkTrans и ее отдавать в метод уже (в самом методе и чуть ниже придется поменять код, так что обрабатывать запись, а не курсор, но это не кажется сложным). |
|
27.05.2022, 11:05 | #10 |
Участник
|
Цитата:
Сообщение от Masel
Я тут в очередной раз занимался оптимизацией разноски розничных продаж и наткнулся еще на пару возможных улучшений.
<...> 2. В классе Tax метод lineTaxAmount. В начале метода проверяется что в таблице есть записи немного экзотическим методом, считая их количество. Я поменял так, хотя можно было наверное вообще убрать запрос, меня пока и так устраивает. X++: if (this.taxParameters().TaxSpecifyLine) { if (this.taxParameters().TaxSpecifyLine) { //+ sergey.m 03.02.2022 FRE_20421639_001 //select count(RecId) from taxWorkTrans; select firstOnly RecId from taxWorkTrans; //- sergey.m 03.02.2022 FRE_20421639_001 if (taxWorkTrans.RecId > 0 && !this.useSubLedgerJournalLines()) { // Posting out of TmpTaxWorkTrans \Classes\TaxPost\saveAndPost X++: public void saveAndPost(LedgerPostingController _ledgerPostingController, SelectableDataArea _companyToPost = curext()) { this.initLedgerPosting(_ledgerPostingController); //+ Abramov_ 27.05.2022 TSK0000280_08 //select count(RecId) from taxWorkTrans; select firstonly RecId from taxWorkTrans; //- Abramov_ 27.05.2022 TSK0000280_08 if (taxWorkTrans.RecId > 0 && !this.useSubLedgerJournalLines()) При создании строк накладной покупки в PurchInvoiceJournalCreate.createJournalLine() поле vendInvoiceTrans.LineAmount рассчитывается без учета экземпляра класса Tax, который был рассчитан на предыдущем шаге (в PurchInvoiceJournalCreate.initTotals()). Т.е. в initTotals мы формируем кэш проводок TaxUncommitted, и, если не передать Tax, мы делаем лишние запросы к тому же TaxUncommitted. X++: //+ Abramov_ 27.05.2022 TSK0000280_08 //vendInvoiceTrans.LineAmount = vendInvoiceInfoLine.lineAmountExclTax(vendInvoiceJour.InvoiceDate); vendInvoiceTrans.LineAmount = vendInvoiceInfoLine.lineAmountExclTax(vendInvoiceJour.InvoiceDate, this.parmTax()); //- Abramov_ 27.05.2022 TSK0000280_08 Еще мы провели эксперимент с переводом TmpTaxWorkTrans в TempDB (и обновлением существующей функциональности соответственно). Пришли к выводу, что без существенного рефакторинга кода от версии TmpTaxWorkTrans в TempDB толку нет, т.к. производительность расчета налогов упала ровно в два раза. Последний раз редактировалось DarkSpirit22; 27.05.2022 в 11:29. |
|
|
За это сообщение автора поблагодарили: Logger (5). |
03.05.2011, 14:30 | #11 |
Участник
|
Хм. Про вытеснение времянок с памяти на диск я и не подумал.
Хотя все равно как-то косячно. Думаю что можно переделать алгоритм так чтобы зависимость от числа строк была линейной. Если получится - отпишу. |
|
03.05.2011, 14:34 | #12 |
Moderator
|
Я бы побробовал для начала выгрузить tmptaxWorkTrans в настоящую таблицу с неким guidом в качестве дополнительного поля. Потом прогнал бы по ней все операции, а потом загрузил бы назад в tmpTaxWorkTrans. Изврат конечно - но может и поможет. Особенно если вставлять через recordInsertList.
|
|
03.05.2011, 14:40 | #13 |
Участник
|
Ого !
Радикально ! А я думал в мап запихнуть - чтобы гарантировать наличие данные в памяти. Или в RecordSortedList (правда хрен его знает - сбрасывает он данные на диск или нет. Если данных не много - поиск по нему идет примерно с той же скоростью что и по мапам) |
|
03.05.2011, 15:07 | #14 |
Moderator
|
Цитата:
Вообще может быть еще можно построить кучу индексов по временной таблицe, оптмизированых под именно этот запрос |
|
29.11.2011, 11:26 | #15 |
Участник
|
Поковырял еще тему с оптимизацией
см. еще axdaily: SQL temporary tables in AX 2012 Оказалось, что нелинейность прячется не в классе Tax а при прописывании налогов в строки документа. Например для класса PurchFormLetter_Invoice в методе writeTaxAmount_W есть код X++: invoiceTrans.initFromTaxWorkTrans_RU(this.tmpTaxWorkTrans(vendInvoiceJour.RecId),
tablenum(PurchLine),
0,
invoiceTrans.InventTransId); Получается что мы перебираем N строк накладной и на каждой строке копируем времянку через while select { buffer.insert() } т.е. тоже выполняем N шагов. Итого сложность алгоритма N^2 - квадратичная. Исправить можно например так : X++: protected void writeTaxAmount_W() { VendInvoiceTrans invoiceTrans; CRSEFeatures_W features = CompanyInfo::features_W(); TmpTaxWorkTrans GRD_locTmpTaxWorkTrans; //+GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 if (features != CRSEFeatures_W::PL && features != CRSEFeatures_W::RU) return; if (! TaxParameters::find().TaxSpecifyLine) return; // GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 --> GRD_locTmpTaxWorkTrans = this.tmpTaxWorkTrans(vendInvoiceJour.RecId); // GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 <-- while select forupdate invoiceTrans index hint InvoiceIdx where invoiceTrans.PurchID == vendInvoiceJour.PurchId && invoiceTrans.InvoiceId == vendInvoiceJour.InvoiceId && invoiceTrans.InvoiceDate == vendInvoiceJour.InvoiceDate && invoiceTrans.InternalInvoiceId == vendInvoiceJour.InternalInvoiceId && invoiceTrans.NumberSequenceGroup == vendInvoiceJour.NumberSequenceGroup { invoiceTrans.initFromTaxWorkTrans_RU( // GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 --> //this.tmpTaxWorkTrans(vendInvoiceJour.RecId), GRD_locTmpTaxWorkTrans, // GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 <-- tablenum(PurchLine), 0, invoiceTrans.InventTransId); invoiceTrans.doUpdate(); } } На документах из 1000 строк экономия времени составила примерно 10 минут. Для документов из 100 строк не замерял, но думаю что немного. Интересно что эта ошибка тянется еще с Ax 3.0 |
|
|
За это сообщение автора поблагодарили: lev (5), gl00mie (10), someOne (6). |
29.11.2011, 13:04 | #16 |
Участник
|
Цитата:
SalesFormLetter_Invoice все то же самое. Ужас какой то... Надеюсь "Локализаторы" обратят внимание и исправят в новых версиях! Для тех кто работает с большим количеством строк в заказах или закупках эта оптимизация будет очень полезна. |
|
29.11.2011, 11:45 | #17 |
Участник
|
Кстати, иногда можно слышать мнение, что кешированием в памяти чего-либо можно решить большинство проблем производительности.
К сожалению, это не всегда так. Серебряной пули нет. Кривой алгоритм может поставить на колени даже память с мощным процессором. Второй пример нелинейности при обработке накладной по закупке : Стек вызовов : ... PurchFormLetter.run PurchFormLetter.createJournal PurchFormLetter.insertJournal PurchFormLetter_Invoice.updateNow PurchFormLetter_Invoice.updateInventory MarkupAdjustment::adjustInvoice ledgerVoucherObject.postCurrencyDiff в методе ledgerVoucherObject.postCurrencyDiff есть вызовы методов ledgerVoucherObject.listCurrencyAmountCur ledgerBondClient.log2Table ledgerBondClient.currentLog Время выполнения каждого метода также квадратично зависит от числа строк в накладной. Но тут все немного хуже чем в первом случае, потому что для накладной из 1000 строк у меня получились дублирования мапов с одновременной фильтрацией путем перебора 8-9 тысяч элементов. Этого уже не выдерживает даже супербыстрый проц и память. Самое интересно что в мооем примере не было никаких накладных расходов и судя по всему выполнение метода MarkupAdjustment::adjustInvoice можно было вообще исключить. Пока добавил такую оптимизацию : X++: /// <summary> /// Calculates MST difference per transaction and generates an equalizing transaction. /// </summary> /// <param name="_ledgerVoucher"> /// The ledger voucher for the transaction. /// </param> /// <param name="_ledgerPostingType"> /// The posting type for the transaction. /// </param> /// <param name="_ledgerAccount"> /// The ledger account for the transaction. /// </param> /// <param name="_dimension"> /// A dimension. /// </param> /// <param name="_sourceTableId"> /// The table ID of the posting source. /// </param> /// <param name="_sourceRecId"> /// The record ID of the posting source /// </param> /// <param name="_transactionTxt"> /// A transaction text. /// </param> /// <param name="_level"> /// The level; option. /// </param> public void postCurrencyDiff( LedgerVoucher _ledgerVoucher, LedgerPostingType _ledgerPostingType, LedgerAccount _ledgerAccount, Dimension _dimension, tableId _sourceTableId, recId _sourceRecId, TransactionTxt _transactionTxt, Integer _level = 0) { LedgerVoucherTransObject ledgerVoucherTransObject; RecordSortedList rsL; TmpLedgerTrans tmpLedgerTrans; boolean more; // <GEEU> TmpLedgerBondLogTable_RU logTable; LedgerBondTransObject_RU bondTransObject; // </GEEU> boolean GRD_LoadedBondInfo = false;//+GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 rsL = this.listCurrencyAmountCur(_level); // <GEEU> // GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 --> if (GRD5::isR4719_2()) { // вынесли этот код (блок else) внутрь цикла ниже, так как вызовы ledgerBondClient.currentLog() и ledgerBondClient.log2Table( // могут быть сложны для расчета при большом числе строк в документе (нелинейные зависимости от числа строк содержатся ) // а их результат иногда бывает и не нужен, поэтому раньше времени их не вызваем, а только по мере необходимости, т.е. на первом шаге цикла } else { if (ledgerBondClient) { logTable = ledgerBondClient.log2Table(ledgerBondClient.currentLog()); } } // GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 <-- // </GEEU> for (more = rsL.first(tmpLedgerTrans); more; more = rsL.next(tmpLedgerTrans)) { ledgerVoucherTransObject = LedgerVoucherTransObject::newCreateTrans( this, _ledgerPostingType, _ledgerAccount, _dimension, tmpLedgerTrans.CurrencyCode, -tmpLedgerTrans.AmountCur, _sourceTableId, _sourceRecId); ledgerVoucherTransObject.parmTransTxt(_transactionTxt.txt()); _ledgerVoucher.addTrans(ledgerVoucherTransObject); // <GEEU> if (ledgerBondClient && tmpLedgerTrans.AmountCur) { // GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 --> if (GRD5::isR4719_2()) { if (!GRD_LoadedBondInfo) { GRD_LoadedBondInfo = true; if (ledgerBondClient) { logTable = ledgerBondClient.log2Table(ledgerBondClient.currentLog()); } } } // GRD_R4719_SpeedUpFormLetter_pkoz, pkoz, 24.11.2011 <-- bondTransObject = ledgerBondClient.bondTransObject(ledgerBondClient.lastVrefId()); while select logTable where logTable.CurrencyCode == tmpLedgerTrans.CurrencyCode && logTable.Crediting != bondTransObject.remainCrediting() { ledgerBondClient.bondVRef2VRef(bondTransObject.vrefId(), logTable.vRef); if (bondTransObject.remainAmountCur() == 0) { break; // </GEEU> } } // <GEEU> } } } // </GEEU> P.S. Вообще, грустно, что обработка накладных совсем не оптимизировалась на обработку больших объемов информации. |
|
|
За это сообщение автора поблагодарили: Wamr (10). |
29.11.2011, 11:46 | #18 |
Участник
|
GRD5::isR4719_2() - это признак "включенности" изменений. Должен возвращать true
|
|
29.11.2011, 15:12 | #19 |
----------------
|
|
|
|
За это сообщение автора поблагодарили: Logger (3), lev (5), someOne (6). |
29.11.2011, 15:47 | #20 |
Участник
|
|
|
Теги |
faq, tax, налоги, оптимизация, производительность |
|
Похожие темы | ||||
Тема | Ответов | |||
Вызов метода базового класса | 15 | |||
jerry-dynamics: tax codes | 0 | |||
Вызов класса из другого класса | 9 | |||
передача курсора в два класса | 3 | |||
Запустить метод класса | 2 |
|