Intel® Software Network Knowledge Base Wiki


Constructing Nav Tree
One Moment...

(refresh menu)



 
Welcome, Guest | Quick Login | Register

.NET


How to Apply Application-Level Tuning on the ASP.NET Platform

Version 7, Changed by LINDA SWINK on 3/21/2008
Created by: KYLEX.S.LEWIS@INTEL.COM

Challenge

Apply application-level tuning to a hypothetical ASP.NET* application. After system-level bottlenecks have been alleviated, developers can further increase performance through changes in the application.

Solution

Optimize caching in the application; caching is the single biggest source code or configuration file optimization that you can perform in most Web applications. Concentrate on achieving better use of the platform features, rather than improving the efficiency of application code, since the platform automates most of the common programming tasks that used to be handled by the application code, such as memory management and session-state handling.

Overall, there are two types of caching: output caching and data caching. Output caching can involve either caching entire Web pages or caching fragments of Web pages. In .NET*, this caching is unlike caching of static Web pages, because it works with dynamic content (pages built with ASP.NET*). Each time a page (or a fragment of a page) is requested with the same dependencies as another recent request, the information is delivered from cache.

Data caching, which is also called Object Caching, is the ability to cache the output of any method. Typically, it is used for methods with a high performance cost, such as methods that access a database. Until the advent of .NET, many serious applications would build their own data cache, but few did it well.

If properly used, .NET’s caching options can provide huge performance improvements. Proper use of the cache results in less object creations, less processing, and most important of all, lower dependency on slow system resources (such as disk I/O) or external facilities such as databases, remote objects, or Web services.

Using the object cache is easy; using it properly is difficult. The object cache can be used wherever a method returns an object. There are also several forms for inserting data into the cache, including versions that allow you to set expiration times for the item. In our example, the cache has a simple key as the parameter to the fetchMyData method:

DataView Source;
// See if the object is in cache
Source = (DataView)Cache["MyDataKey”];
// If object isn’t in cache, get
if(Source == null )
{
   Source = fetchMyData(“MyDataKey”);
   // Put data in cache
   Cache["MyDataKey"] = Source;
}
// use the data
MyDataGrid.DataSource = Source;
MyDataGrid.DataBind();

The cache key must contain all the dependencies that would affect the output of the method to be cached. The key is always derived from some or all of the parameters to the method being called; however, it may also use other variables that can affect the result. For example, fetchMyData might contain internal state information – such as MyUser information – such that it would return a different value for another user. In this case, it is prudent to include the user ID as part of the key to the cache.

In addition to simple variables, dependencies can also include files, directories, or the keys for other objects in the cache.

Incorrectly identifying the dependencies for caching can result in incorrect program output or low cache hit rates. Determining these dependencies requires a programmer’s skill (i.e., do not expect to see tools to automate the process anytime soon).

Using the cache properly to optimize performance is not a simple matter. Two performance problems are prevalent in many .NET applications:

Under-caching. To under-cache is to fail to take advantage of caching where it would be of benefit.

Over-caching: Using the cache where you shouldn’t is known as over-caching.

Under-caching is caused by the fact that unlike a processor cache, .NET caching does not happen automatically. If you have not implemented caching, you are missing out on performance.

The second problem, over-caching, is equally serious. On applications that abuse the object cache, the object cache itself becomes a major bottleneck that limits the performance of the application. Part of the problem with over-caching is that putting something in cache might imply “kicking-out” something else that is needed. With .NET, a bigger part of the problem is that the cache object is a synchronized (thread-safe) collection; therefore, it can become a major source of contention when multiple threads are trying to access it at the same time.

Proper use of the .NET cache includes putting all things in cache if they will be used again, and only if they will be used again. The best way to find these problems would be to have a tool do all the work; unfortunately, that is not possible today. These tools are probably coming in the near future, but they will require additional instrumentation in the .NET framework.

In the meantime, here are some strategies to consider and some tips on how each might be applied in various situations.

Start from the ground up: implement caching only where appropriate.

Observe what is removed from cache.

Manually instrument every use of cache to determine hit/miss rates.

What follows is a description of each approach and an analysis of each from the perspective of accuracy, ease-of-use, and ease-of-implementation.

Start From the Ground Up. In this approach, start without any caching, and then use strategies to implement caching only where it is strongly indicated. This approach is applicable if you have not implemented any caching or when you have caching that may be improperly implemented. It is easier to find under-caching than it is to find over-caching.

The implementation effort is very high with this approach, especially for programs that already have caching. First, you must remove (comment out) all caching, then you must use a methodology to decide where to cache, and finally you must re-implement caching where appropriate. One way to find out where to cache is to start with the objects that have the highest creation rates, implement caching there, and observe the hit rate.

When applying this approach, keep in mind that string and other objects will show up as high usage in many places. You must determine whether the same data is ever likely to be returned, or time will be wasted implementing caching where the hit rate will be low. Furthermore, incorrectly implementing caching can result in incorrect program output or low cache hit rates. You must correctly identify dependencies to implement proper caching; unfortunately, determining dependencies is not a trivial task. Dependencies may be more than just parameters to the method; they may include files or time information.

Ideally, you should only cache where it will yield the most benefits. In reality, your results will depend a lot on the skill and effort of the implementer.

Observe What is Removed From Cache. If the code already makes extensive use of caching, use this technique to determine if you are caching when you should not or have implemented caching incorrectly. For this technique, you can use the CacheItemRemovedCallBack to tell when an item is removed and to track the type of objects being evicted. The most evicted objects might represent objects that were placed into the cache but are never used, because unused items in cache will get evicted due to the Least Recently Used algorithm the cache employs.

There are three problems with this technique. One is the complexity of implementation. You will have to implement cache (with the CacheItemRemovedCallback) in all the places where it is likely to be helpful. You will also need to implement the actual method that will be called by the callback mechanism. You will then have to figure out where in the program these callbacks are happening. Unless you use a different callback, you will only have the name and value of the key to guide you.

Another problem is that there is no way of knowing how long these items were used in cache before they were evicted. They might have been worth caching. They might have been used for a while then evicted when they expired or a dependency changed.

Manually Instrument Use of Cache. In this approach, you modify the application’s source code to instrument every use of cache to determine hit/miss rates, hit/miss counts, and the ratio for that use. For example:

DataView Source;
Source = (DataView)
Cache["MyDataKey"]; // Cache lookup.
if(Source != null )
   // Add instrumentation
   // to count cache hits
   XXXXXXXXX();
else {
   // Add instrumentation to
   // count cache misses
   YYYYYYYY();
   // Get the data
   Source = fetchMyData(“MyDataKey”);
   // Put the data in cache
   Cache["MyDataKey"] = Source;
}
// use the data

 

This is very accurate: it simulates processor cache counters to show where cache is effective and where it is not. Unfortunately, extensive code changes are required to implement this instrumentation, and they can be more complicated in certain code situations. First, caching has to be implemented on every likely candidate. Implementing caching is difficult, because although it is only a couple lines of code, you must correctly identify dependencies.

Second, since C#* does not provide macros like __FILE__ and __LINE__ or the ability to build macros that would combine these built-in counters with instrumentation, implementing the cache manually is labor-intensive.

This item is part of a series on optimizing applications for the ASP.NET platform, which is introduced in a separate item, How to Optimize Applications on the ASP.NET* Platform.

Source

Developing and Optimizing Web Applications on the ASP.NET Platform

 



Served
23 Knowledge Bases
605 Pages
Search
Powering Up Search...


Vote on this Page

Tags For This Page
Loading Tags..

Tag This



Additional legal information