Using SysGlobalObjectCache (SGOC) and understanding it’s performance implications
The SGOC is a kernel-managed cache. This is a new type of cache available in Dynamics AX 2012. Unlike the SysGlobalCache in AX2009 and older versions which has the session scope, SysGlobalObjectCache is truly global in nature. The data stored from one user connection is available for all the users.
SGOC stores Key-Value pairs. Both Key and Value in the SGOC must be containers. This is because containers are passed by value and the content stored in them is not influenced by the changes happen to the variables externally.
There are some basic behavior differences between SGOC and the other caches.
- Unlike kernel data caching, application must manage updating/flushing the cache. The kernel has no way of knowing when some cached data is no longer valid, so it is the application’s responsibility to manage this and clear at the appropriate times.
- Unlike kernel data caching, checking to see if something exists in the cache when it doesn’t will not cause an RPC to query the data from the server. With normal caching on a table there was no way to avoid this extra call on a cache miss. The SGOC will not cause any extra calls. The clearing of data is piggy-backed on other existing RPC calls between machines
- Unlike the SysGlobalCache, the SGOC will propagate “clear” calls to all clients and other AOS instances. So if the application clears data from a cache scope on one client, all other clients and AOS’s will be cleared. The same happens if the application clear it on an AOS. This is useful when a user performs an operation which invalidates some data in the cache.
SGOC is an LRU cache. When the cache is full, the least recently used element will be removed to accommodate newer element. Sizing the SGOC correctly will pay significant performance improvement over poorly sized SGOC setting. The number of elements Global Object cache can hold is defined in Server Configuration form under performance Optimization fast tab.
Simple Code Example
Here is a simple code example to illustrate the behavior/usage of SGOC. Use always an application class to abstract the access and manipulation of SGOC for specific scopes. Example ‘DimensionCache’ or ‘PriceDisc’ Class.
public static int64 GetCustOpenSalesOrderCounts(str 20 _myCustId)
{
SalesTable salesTable;
container conSGOC;
int64 salesOrderCounts;
// Create a new instance of the SGOC class. Note that this will "connect to" the current instance of the global cache, so values pushed in with one instance will be available with other instances.
SysGlobalobjectCache sgoc = new SysGlobalObjectCache();
//get the value from cache for the (Scope, Key) combination
conSGOC = sgoc.find('CustOpenSalesOrderCounts', [_myCustId]);
//Check the return value of the container for conNull, if the container is null, there is no value exist for that scope,key combination
if(conSGOC == conNull())
{
//Do the business process here
select count(RecId) from salesTable where salesTable.CustAccount == _myCustId &&
salesTable.DocumentStatus == DocumentStatus::None;
salesOrderCounts = salesTable.RecId;
// Push an element into the cache. The first parameter is the scope, which identifies the cache. The second parameter is the key for looking up and the third parameter is the value. Both Key and values are containers by type.
sgoc.insert('CustOpenSalesOrderCounts', [_myCustId], [salesOrderCounts]);
}
else
{
salesOrderCounts = conPeek( conSGOC, 1);
}
return salesOrderCounts;
}
public static void main(Args _args)
{
int counter;
str 20 myCustId;
int64 salesOrderCounts;
for (counter = 1; counter <= 25; counter++)
{
myCustId = 'E'+int2str(10000+counter);
salesOrderCounts = DemoSGOCServer::GetCustOpenSalesOrderCounts(myCustId);
info(myCustId+' : '+int642str(salesOrderCounts));
}
}
Example from Product
DimensionDefaultingService:
server private static LedgerDimensionAccount serverCreateLedgerDimension(
RecId _ledgerDimensionId,
DimensionDefault _dimensionDefault1 = 0,
DimensionDefault _dimensionDefault2 = 0,
DimensionDefault _dimensionDefault3 = 0)
{
container cachedResult;
XppILExecutePermission xppILExecutePermission;
// get the value from cache for the (Scope, Key) combination
cachedResult = DimensionCache::getValue(
DimensionCacheScope::DefaultingCreateLedgerDimension,
[_ledgerDimensionId, _dimensionDefault1, _dimensionDefault2, _dimensionDefault3]);
// Check the return value of the container for conNull, if the container is null, there is no value exist for that scope,key combination
if (cachedResult == connull())
{
// Main API should have already short-circuited
Debug::assert(_ledgerDimensionId != 0);
xppILExecutePermission = new XppILExecutePermission();
xppILExecutePermission.assert();
// Do the business process.
cachedResult = runClassMethodIL(
classStr(DimensionDefaultingService),
staticMethodStr(DimensionDefaultingService, createLedgerDimension),
[_ledgerDimensionId, _dimensionDefault1, _dimensionDefault2, _dimensionDefault3]);
CodeAccessPermission::revertAssert();
// Push an element into the cache.
DimensionCache::insertValue(
DimensionCacheScope::DefaultingCreateLedgerDimension,
[_ledgerDimensionId, _dimensionDefault1, _dimensionDefault2, _dimensionDefault3],
cachedResult);
}
return conpeek(cachedResult, 1);
}
DimensionCache:
public static container getValue(DimensionCacheScope _scope, container _key)
{
SysGlobalObjectCache c;
if(classfactory)
{
c = classfactory.globalObjectCache();
}
else
{
c = new SysGlobalObjectCache();
}
return c.find(DimensionCache::getCacheScopeStr(_scope), _key);
}
public static void insertValue(DimensionCacheScope _scope, container _key, container _value)
{
SysGlobalObjectCache c;
if(classfactory)
{
c = classfactory.globalObjectCache();
}
else
{
c = new SysGlobalObjectCache();
}
c.insert(DimensionCache::getCacheScopeStr(_scope), _key, _value);
}
Performance impact and memory usage:
Here is a small test which stores or access elements from 10000 to 200000. The table compares 2 sets of tests, one when cache size is set to 10000 while on the other it is increased to 200,000.
Number of Elements |
CacheSize = 10K |
CacheSize = 200K |
||
First Time * |
Second time or latter ** |
First Time |
Second time or later |
|
milliSeconds |
milliSeconds |
milliSeconds |
milliSeconds |
|
10000 |
387 |
166 |
395 |
172 |
25000 |
1014 |
1025 |
797 |
441 |
50000 |
2254 |
2197 |
1598 |
938 |
100000 |
4743 |
4803 |
3419 |
2049 |
200000 |
9088 |
9132 |
8424 |
5023 |
* ‘First Time’ - The Key does not exist in the cache for this scope. Business process is executed to find the value and ‘Key, Values’ are inserted into the SGOC.
** ’Second time or Latter’ – The key for this scope is checked either second time or more. When the SGOC is sized correctly it should find it in the cache. If it is not sized correctly, the element might have been removed by the LRU. If it does not find it in the cache business process is executed again to find the value and ‘key, values’ are inserted back into the cache.
When the cache is undersized elements are removed from the cache to accommodate new elements. The above test does not really justify the usage of SGOC as the performance gain is smaller, unless this is very frequently used or reduces lot of chattiness or Database calls.
When you try to cache the result of complex business logic which is relatively static in nature, you get a significant performance gain you find the values in the cache. The following test result exhibits the importance of sizing the cache adequately.
Number of Elements |
CacheSize = 10K |
CacheSize = 200K |
||
First Time |
Second time or latter |
First Time |
Second time or latter |
|
milliSeconds |
milliSeconds |
milliSeconds |
milliSeconds |
|
10000 |
227118 |
178 |
235184 |
177 |
25000 |
568833 |
563738 |
482203 |
437 |
Other frequent question often comes up is how does SGOC affects the memory footprint. It purely depends on the size of the element you are storing in the cache and number of elements. Storing few integer fields and date fields in SGOC, it used up about 28MB to store 100,000 elements. Whereas when a packed SalesTable buffer is stored, SGOC used little over 200MB.
Best Practices
The SGOC is a very useful tool in some situations, but may not be the appropriate tool in many cases.
DOs
- Size the SGOC correctly. When it is undersized, frequently elements will be removed and added to SGOC. Removing of the elements has higher overhead.
- Use SGOC in cases where caching will reduce intensive calculations, RPCs, database calls.
- Use the SGOC in cases where the same inputs to a method will always return the same result
- Provide a wrapper around SGOC when a subsystem uses it for similar areas. Example: DimensionCache class.
DON’Ts
- Do not use the SGOC if simple kernel data caching will cover your scenario.
- Do not cache results/data that will be frequently changed or updated.
- Do not check if a value exists in cache before retrieving it. Instead try to retrieve it, then check if the result was connull() or not. It will improve performance and may avoid race condition.
- Do not aggressively use the remove() method of the SGOC. Using this frequently will quickly become a performance bottleneck.
Comments
- Anonymous
March 27, 2012
Is the SysGlobalObjectCache truly global? I have a scenario where I'm utilizing the global object cache on the web through services - but I cannot access that cache from a Windows client session. Is that expected behaviour or can I do something to change this? - Anonymous
March 27, 2012
Yes, it is. The new "SysGlobalObjectCache" is different from the "SysGlobalCache" which was available to the same session only. This new cache, introduced in AX2012, is available to all sessions and users. If you want it to be shared across all sessions use SysGlobalObjectCache instead of SysGlobalCache. - Anonymous
March 27, 2012
Thanks for a quick response. I am referring to the SysGlobalObjectCache and I am using that (NOT SysGlobalCache). I did some further testing and realized that if I used runClassMethodIL to run it in IL from my Windows client, I could access the same SysGlobalObjectCache as my web sessions are using. The aim of my work was to be able to tap into the global cache that my web sessions were using.So - indeed - there is a different version of the SysGlobalObjectCache residing on the .NET side. - Anonymous
March 27, 2012
Are you using SysGlobalObjectCache? If you are using sysGlobalObjectCache, the cache will be shared between web sessions and would be available for the windows client session too as long as they are pointing to the same AOS instance. If you use a aos load balancer/wcf load balancer and if your session is connected to different AOS, you may see a cache miss, if it is not cached on that AOS. - Anonymous
March 27, 2012
Definitely using SysGlobalObjectCache - yes. One AOS, so no load balancing or different servers.Scenario 1:... cacheManager = DwsCacheManager::construct("DwsLog"); list = cacheManager.readLog();...instantiates my cache manager - which uses SysGlobalObjectCache:class DwsCacheManager{ SysGlobalObjectCache sgoc; GlobalObjectCacheScope scope;}public void new(GlobalObjectCacheScope _scope){ scope = strFmt("%1.%2", _scope, curext()); sgoc = new SysGlobalObjectCache();}readFromLog then just reads from a certain cache. In scenario 1 - I get no cache generated from my web sessions, however - if I add to the cache from a Windows client session - I get that cache.Scenario 2:.... list = DwsCacheManager::ReadLogServer();....static server List ReadLogServer(){ container c; List list; XppILExecutePermission perm = new XppILExecutePermission(); perm.assert(); c = runClassMethodIL(classStr(DwsCacheManager), staticMethodStr(DwsCacheManager, ReadLogServerIL), conNull()); list = list::create(c); return list;}static server container ReadLogServerIL(container _c){ container c; List list; DwsCacheManager cacheManager = DwsCacheManager::construct("DwsLog"); list = cacheManager.readLog(); c = list.pack(); return c;}Will return the cache from my web sessions (but not from my client Windows sessions). - Anonymous
September 25, 2012
When using classFactory.globalObjectCache() from the client tier, you will get a disconnected instance of the cache. This one lives only on the client - and is session wide only. Consider using appl.globalObjectCache insteadThat may be what you are experiencing. - Anonymous
November 28, 2012
Can the SysGlobalObjectCache be leveraged in Dynamics CRM 2011 as well?thank you,Filip - Anonymous
March 26, 2014
Hi - from the article: "The number of elements Global Object cache can hold is defined in Server Configuration form under performance Optimization fast tab."Do you have any recommendations for these settings - especially the "Entire table cache size" and "Global Object Cache elements" in respect to AOS memory. If we have plenty of memory on the AOS, would you recommend changing any of the settings? - Anonymous
March 26, 2014
@Filip : No, the SGOC is for Dynamics AX 2012+ only. - Anonymous
March 27, 2014
@Ray - thank you for your comments. I have noticed some performance benefits by increasing the value in "Entire table cache size" - "Size beyond which the table cache will be flushed to hard disk (in Kilobyte)"What I gather from the help text is that I am increasing the amount of cache for procedures that already rely on using the SGOC - is that correct?Also - do you have any recommendations for that parameter? - Anonymous
May 08, 2014
Hi Ray"UsageCount" method return the current number of references that the object has.msdn.microsoft.com/.../sysglobalobjectcache.aspxI didn't find any way to count the number of elements.Regards - Anonymous
May 08, 2014
Thanks Denis, you are right. I found no method that exposes that. Even a wrapper to keep track will not work since the kernel can remove items.