Preface

The Alpha® edition of the Microsoft® Visual C++™ 6.0 Development System brings the power of object-oriented programming to Microsoft Windows NT™ version 4.00 and later, on the Alpha platform. The Alpha edition of Microsoft Visual C++ contains all the features of the development environment, as well as language and class-library support, found in the x86 edition of Visual C++.

With well engineered source code, you’ll be able to port most project and source files between the x86 and Alpha platforms with ease. Because the C/C++ language provides low-level access to the hardware, however, there are some ways in which the development environments differ (mostly in debugging), and some special areas of concern in portability and application performance. These issues are discussed in this book.


Using This Book

This book describes product differences and portability issues between the Alpha and x86 platforms. You will find this information useful regardless of whether you are porting existing code from the x86 platform or have been programming on the x86 platform and are writing new applications for one or more Alpha platforms.

Note The documentation for the x86 edition of Visual C++ applies to the Alpha edition as well, except where this manual specifically states otherwise.

You should read this book, especially the first chapter, when writing C/C++ code in a Alpha environment for the first time. Depending on how you use Visual C++ and what kind of programming you do, you may not need to read all the chapters. You can glance at the chapters now and come back to them when you need to do related work.

Chapter 1, Product Overview, provides a brief description of how the editions of Visual C++ for the Alpha and x86 platforms differ in major capabilities.

Chapter 2, Working in the Alpha Visual C++ Environment, describes how to start porting code by copying and adapting project and source files from the x86 platform. This chapter also describes the differences between the development environments.

Chapters 3 and 4 discuss portability issues and command-line changes (including external makefiles). These chapters present information for developers who need detailed information on how to move their applications to Alpha platforms, as well as optimize the performance of applications on Alpha platforms.

Chapter 5, Features Specific to Alpha, discusses features of Visual C++, and specific programmatic concerns that are unique to the Alpha platform.

Chapter 6, Using the Alpha Inline Assembler, discusses the use of inline assembly in the Alpha version of Visual C++. This chapter also provides examples of how to properly write assembly code for these platforms.


Document Conventions

This book uses the following typographic conventions:

Example | Description
STDIO.H | Uppercase letters indicate filenames, segment names, and terms used at the operating-system command level.
char, _setcolor, _ _far | Bold type indicates C and C++ keywords, operators, language-specific characters, and library routines. Within discussions of syntax, bold type indicates that the text must be entered exactly as shown.
Many constants, functions, and keywords begin with either a single or double underscore. These are required as part of the name. For example, the compiler recognizes the _ _cplusplus manifest constant only when the leading double underscore is included.
_ _function
__function
 Many language elements are preceded by a double underscore. For reading clarity, a space is added between the 2 underscores within descriptive text. For accuracy, however, the space between the underscores is omitted in actual code.
expression | Words in italics indicate placeholders for information you must supply, such as a filename. Italic type is also used occasionally for emphasis in the text.
[option]
Items inside square brackets are optional.
#pragma pack {1 | 2} | Braces and a vertical bar indicate a choice among two or more items. You must choose one of these items unless square brackets ([ ]) surround the braces.
#include <io.h>
 This font is used for examples, user input, program output, and error messages in text.
CL [option...] file... | Three dots (an ellipsis) following an item indicate that more items having the same form may appear.
while()
{
. . .
}
 A column or row of three dots indicates that part of an example program has been intentionally omitted.
ctrl+enter | Small capital letters are used to indicate the names of keys on the keyboard. When you see a plus sign (+) between two key names, you should hold down the first key while pressing the second.
The carriage-return key, sometimes marked as a bent arrow on the keyboard, is called ENTER.
"C string"
 Some C constructs, such as strings, require quotation marks. Quotation marks required by the language have the form " " or ' ' rather than ‘ ’.
Dynamic-Link Library (DLL) | The first time an acronym is used, it is spelled out, followed by the acronym in parentheses.


Chapter 1 Product Overview

Visual C++ is a universal development environment that can operate equally well on any 32-bit Windows® platform. The look and feel of the development environments for the x86 and the Alpha editions of Visual C++ are almost identical. Consequently, if you have used Visual C++ on the x86 platform, there is very little to learn when using the Alpha version of the product.

By following the guidelines for writing portable code you can compile large amounts of code on any of the supported Alpha platforms without experiencing errors or performance penalties.

This chapter summarizes the differences between the x86 and Alpha editions of Visual C++ 6.0. The following topics are covered:

  • Features in the Alpha Edition

  • Restrictions in the Alpha Edition

  • Differences in the Development Environment


Features of the Alpha Edition

The Alpha edition of Visual C++ supports unique features not found in the x86 edition. Because these features are specific to the Alpha architecture, use them with care if you intend to recompile applications on the x86 platform. The #ifdef directive, along with the appropriate platform specifier, is recommended for using Alpha-specific features while maintaining portable code. Alternatively, you can isolate platform-specific features into separate modules.

Chapters 3, 4, and 5 provide more detail on the features summarized in this section.


Selectable Floating-Point Operation Precision

Within the Alpha architecture, floating-point operations can be carried out in either single-precision (32-bit) or double-precision (64-bit) format. In general, single-precision operations are marginally faster than double-precision operations, with the exception of divide operations, which are significantly slowed by the use of double precision. Within the x86 architecture, temporary floating-point values default to 80-bit precision.

On Alpha platforms, floating-point precision optimization is selectable by using the /Op and /Op– switches. The /Op switch disables precision-losing optimizations, while the /Op– switch allows these optimizations. The /Op– switch allows the use of arithmetic identities that can lose some accuracy. For example, with floating-point hardware, the operation [(a+b)+c] might return a result that is different from [a+(b+c)]. Although this might create slower code, results of small and large values alike are more accurate.


Flexible Handling of Unaligned Data

Depending on the specific Alpha processor(s) you are targeting, applications that perform operations on data that is not naturally aligned on a same-sized boundary (for example, longword on a longword boundary, and quadword on a quadword boundary) can either exhibit poor performance because of the additional processing required to handle the unaligned data, or cause the process to fault and terminate abnormally. You can improve the performance and quality of your applications by avoiding unaligned data operations when possible.

You can avoid alignment problems by disabling structure packing, for example, by using #pragma pack, and by avoiding unorthodox pointer operations such as casting a pointer. Structure packing should be used sparingly, if at all. When structure packing is necessary, however, you can use the pointer qualifier _ _unaligned to access packed data. In addition, on all Alpha systems, you can use the axpalign /enable command to help debug alignment problems in your code.

For a more thorough discussion of data alignment issues, see the Working with Data Alignment Issues section in Chapter 3.


64-Bit Integers

The 64-bit integer type is supported by allx86 as well as Alpha processors. Visual C++ includes the _ _int64 keyword to enable use of signed and unsigned 64-bit integers in your source code. In addition, on Alpha platforms, a number of intrinsic functions and run-time library functions support operations and data conversion involving 64-bit integers.

For more information on 64-bit integers, see the int64 topic in Chapter 5.


Code-Generation Switches

The compiler in the Alpha edition of Visual C++ supports switches that allow you to optimize your compiled code for a particular Alpha processor while maintaining binary compatibility with other Alpha processors. Note, however, that some code-generation switches will create code that will not run on certain platforms.

These switches can be activated either in the Project Settings dialog box under Code Generation, or by manually typing the appropriate /Qa switch into the compiler command line.

The /QAtune and /QAarch switches let you separately set tuning and instruction set options.

For detailed information on code generation switches, see the Processor Optimization Options topic.


Restrictions in the Alpha Edition

A few features supported by the x86 edition of Visual C++ are not supported by the Alpha edition. In most cases, these restrictions are due to the nature of the processor architecture.


Incremental Linking

Incremental linking is enabled by default for no optimization and/or debug builds. This is equivalent to having the /incremental:yes option in the linker command line.

Incremental linking is recommended during application development and debugging, though not for the release version of your product. For optimized builds, disable incremental linking. The resulting executable (.EXE) program will run faster without incremental thunks.

In some rare and seemingly random cases, incremental linking can fail because of erroneous relocation bugs. If you have incremental linking enabled and encounter inexplicable linker errors, try turning off incremental linking.

Incremental linking can be turned on indirectly by the linker command option /debug. Incremental linking can be turned off indirectly by the linker command options /map or /pdb:none.


Inline Assembly

Note that the syntax and structure of inline assembly statements differ between the x86 and Alpha platforms. Refer to Chapter 6, Using the Alpha Inline Assembler, for more information on using inline assembly in your programs.


Macintosh Cross-Development

The Macintosh® cross-development system is only supported on the x86 edition of Visual C++. The Alpha edition of Visual C++ does not include or work with any utilities for the cross-development system.


Differences in the Development Environment

Differences in the development environment have been kept to a minimum, but there are some changes reflecting differences in product capabilities.

Chapter 2, Working in the Alpha Visual C++ Environment, provides more information on porting projects and on differences in the environment.


Writing ANSI-Compliant Code

Visual C++ is designed with American National Standards Institute (ANSI) compliance in mind. While non-ANSI-compliant use of some functions is common on the x86 platform, you can only be assured of your application’s portability if you adhere to the ANSI C and C++ standards.

Another reason for adhering to the ANSI standard is that application performance on Alpha platforms is especially sensitive to ANSI compliance. It is strongly advised that you either refer to the ANSI C and C++ standards when in question about an implementation or use the /Za compiler switch. This switch checks for strict ANSI compliance and issues warnings about all noncompliant code.

Note The ANSI C++ standard is actually written by the ISO/IEC committee, but is published and sold through the ANSI organization. At the time of the writing of this manual, July 1998, the November 1997 draft of the standard has been accepted as a standard. You can purchase the ANSI C++ standard for $50.00 plus shipping by calling ANSI customer service at 212-642-4980. The item number to ask for is ISO/IEC FCD14882.


Calling Conventions

The Alpha edition has a single calling convention, _ _cdecl. Therefore, the /Gd, /Gr, and /Gz switches, which in the x86 version of Visual C++ determine aspects of functions and stack behavior, are unnecessary in the Alpha edition of Visual C++. The Alpha edition quietly ignores these switches.


Debugging

The most notable differences in the development environment occur in the debugging windows. The Register window shows a different register set than does the x86 edition, but it is manipulated in the same way regardless of the platform. The appearance of the Disassembly window is also different, because it shows platform-specific Alpha instructions. The Locals window, which displays addresses of symbols, is also affected by the addressing scheme of the Alpha processor, compiler, and operating system.


Compiler Error Messages Differing from x86

There is an overlap in error message numbers emitted by the compiler for Alpha and for x86 platforms. It is important to note that messages emitted by the Alpha compiler are accurate. In most cases, these messages are self-explanatory. However, if you look up any of the message numbers listed below in the online documentation, you find explanatory text that is applicable only to the x86 compiler.

The following table details the error messages emitted by the Alpha and x86 compilers. For each error, the Alpha number and message are listed first, followed by the x86 compiler message.

Note Variable information has been replaced with the characters “xx.”

Error number | Error Message
1001 | Internal compiler error. Failed Assertion at line xx.
1001 | INTERNAL COMPILER ERROR. Compiler file xx, line xx. Please choose the Technical Support command on the Visual C++ Help menu, or open the Technical Support help file for more information
1002 | System Assertion.
1002 | Compiler is out of heap space in pass 2.
1003 | Must supply -il switch.
1003 | Error count exceeds xx; stopping compilation.
1004 | Must supply -source or -f switch to create source listing.
1004 | Unexpected end-of-file found.
1005 | ex Stack Empty.
1005 | String too big for buffer.
1006 | ex Stack Overflow.
1006 | Write error on compiler intermediate file.
1007 | End-of-file P1/P2 IL list.
1007 | Unrecognized flag xx in xx.
4701 | Code can never be executed at label xx.
4701 | Local variable xx may be used without having been initialized.
4702 | Variable xx is fetched, not initialized.
4702 | Unreachable code.
4703 | Address of a pass-by-value parameter xx was taken.
4703 | xx function too large for global optimizations.
4704 | Number of actual and formal parameters inconsistent in xx.
4704 | xx in-line assembler precludes global optimizations.
4705 | Type of actual argument inconsistent with formal parameter declaration in xx.
4705 | Statement has no effect
4706 | Missing actual argument for corresponding formal xx.
4706 | Assignment within conditional expression.
4707 | Argument passing mechanism does not match formal parameter mechanism for xx.
4707 | xx function too large for global optimizations (xx).
4708 | Routine xx can never be called.
4708 | Ran out of heap at xx.
4712 | Ignoring system register specified in routine's linkage.
4712 | xx used as register - loss of debugging information.
4726 | Volatile access appears unaligned, but must be aligned at run-time to ensure atomicity and byte granularity.
4726 | xx unknown memory-model command-line option.


Other Differences

Occasionally, you may notice small differences in the integrated development environment interface from platform to platform. These differences reflect the addition or removal of features that, because of architectural differences, do not apply to the Alpha version of the product.


Porting Projects from the x86 to the Alpha Platform

You can easily port projects from the x86 platform to the Alpha platform, but some changes are required. You can use the same project (.MAK) and source files, but not intermediate files, which have to be rebuilt. In addition to copying the project files to the new platform and specifying a new target, you must reset project options. Because the Alpha edition of Visual C++ does not accept exactly the same set of options as the x86 edition, the defaults for each platform are assumed.


Chapter 2 - Working in the Alpha Visual C++ Environment

This chapter describes how to use the Visual C++ development environment to port source code to Alpha platforms and also discusses differences in the environment that may affect the way you accomplish certain tasks.

This chapter covers the following topics:

  • Porting Projects to the Alpha Platforms

  • Linking

  • Debugging

  • Disabled Settings in the Development Environment

  • Code Generation Settings


Porting Projects to the Alpha Platform

The first step in porting an application from Visual C++ 6.0 on one platform to another platform is to port the project itself. After that, you can check source code for any changes that may be required, as described in Chapter 3, Porting Visual C++ Source Code.

Porting large projects to new platforms would be cumbersome if you had to re-create the project structure from scratch. Fortunately, the Visual C++ environment lets you specify new target platforms for existing projects. You may need to reset some options, but the file structure is preserved.

To move an existing x86 project to the Alpha platform

  1. Copy all project files to the new platform.

  2. Specify new configurations.

  3. Reset project options.

The next three sections, Copy Project Files, Specify New Configurations, and Reset Project Options, describe these steps.


Copy Project Files

The easiest way to port a project is to preserve its existing tree structure by copying the entire project tree to the new computer. For example, if a project resides in the directory \MyProjects\Portapp on one computer, you can copy the contents of that directory, and all subdirectories, to the \MyProjects\Portapp directory on another computer. In the following illustration, the project is being transferred to the Alpha system over a network. Portable media would work just as well.

Figure 2.1 Project Being Copied


Specify New Configurations

Once you have copied a project to a new platform, opening the project workspace reveals that the target platform is still set to the original platform. Although this is not obvious, you can tell this is so because the Build, Compile, and Execute options are grayed out in the Build menu, as shown in the following figure. Before you can build the application on the new platform, you must first specify one or more new configurations for the project on the new platform.

Note If you open a project created with a previous version of Developer Studio, you are prompted to convert the project to the current format, as shown in the following dialog box.

Figure 2.2 Convert Project to Current Format

To add new configurations on the new platform

  1. From the Build menu, choose the Configurations command.

    Figure 2.3 New Configuration Needed

    The targets for the project are displayed in the Configurations dialog box. The initial configurations are named Win32 Release and Win32 Debug. Although there is no indication that these are x86-specific configurations, they are, since this is the type of system on which the project was created.

    Figure 2.4 Existing Project Targets

To create Debug and Release Alpha targets for this project, you create a new configuration while running the Alpha edition of Visual C++. When running the Alpha edition, the platform for a new configuration is assumed to be the current platform. For example, if you copy an x86 project to an Alpha system, and create a new configuration, the target platform can only be Alpha. You cannot specify another platform. This same behavior applies when moving projects between any two platforms.

To create a new project configuration

  1. In the Configurations dialog box, click the Add button. The Add Project Configuration dialog box appears.

  2. In the Add Project Configuration box, type a new configuration name.

    For example, for a release build, you might enter “Alpha <project name> Release.” For a debug build, you might enter “Alpha <project name> Debug.” The names do not matter, as long as they differ from existing configuration names.

    Figure 2.5 Adding the New Project Configurations

  3. In the Copy settings from: list, select the configuration from which to copy the settings for this configuration and click OK.

    Usually, you will want to copy the settings from a similar configuration. For example, if this new configuration is a release configuration, you will usually copy settings from an existing release configuration.

  4. The Projects dialog box appears with the new project configuration.

    Repeat the process as necessary to create as many configurations as you need.

    Figure 2.6 Project Configuration Dialog Box Completed

Note Once you are done creating the new configurations, it is a good idea to select the original configurations and click the Remove button to delete them. This can help avoid confusion in the future.

  5. Click the Close Button.

    Your project is now configured for the Alpha platform.

  6. From the Build menu, select Set Active Configuration, choose the project configuration you want to use, and click OK.

    Note If you omit this step, you might not be presented with project options appropriate to the Alpha platform when you Reset Project Options next.


Reset Project Options

Because not all settings are transportable across platforms, you should verify your project settings on the new platform.

An important item to check is the optimization level. On the x86 platform, default optimization is set no optimization (/Od), while on the Alpha platform, default optimization is set to favor speed over code size (/Ox). Compiling ported code without ensuring identical optimization levels might result in errors incorrectly attributed to coding rather than to optimization level. For more information on optimization, see the Optimization and Options for Maximum Speed topics in Chapter 4.

To verify your project settings

  1. From the Project menu, choose Settings. The Project Settings dialog box appears.

  2. Review the tabs and categories to ensure that the project settings you want are selected. Pay special attention to the following items:
    • General tab — Review the directories for intermediate and output files.

    • C/C++ and Link tabs — Nonstandard options in the original configurations must be replicated (as applicable) in the new configurations.

    • Custom Build tab — Review for any custom commands that might change between platforms.

    • Pre-link and Post-build Step tabs — Review for any custom commands that might change between platforms.

    Note If the project settings do not seem appropriate for the Alpha Platform (for example, the processor type on the C/C++ Code Generation page is not set to Alpha AXP), it is because you did not complete step 6 in the creating a new project configuration procedure.


Linking

When linking object modules with a large number of exports, it is advisable to avoid the re-creation of the import library if the export list has not changed. This is because generating the import library can account for a large percentage of the link time.

For example, instead of linking with:

link -out:foo.dll -def:foo.def <list of objects>

which creates the import library and export list each time, use:

link -lib -out:foo.lib -def:foo.def <list of objects>
link -out:foo.dll foo.exp <list of objects>

This two-step process creates the import library and export list on the first link. The second link does not create the import library. The first time you link, use both commands. On subsequent links, use only the second link command, thus speeding up the link time. You do not need to use the first link command again until the import library needs to be re-created.

Warning If you change the function name, class, calling convention, return type, or any parameter, the decorated name is no longer valid. You must use the new version of the decorated name and use it everywhere the decorated name is specified. You can view decorated names either by using a compiler listing, or by using the DUMPBIN tool on the .OBJ or .LIB file with the /SYMBOLS option.

Using _ _declspec(dllimport) and _ _declspec(dllexport) is highly recommended because hand written .DEF files will most likely need to be rewritten for Alpha. If you use _ _declspec, the compiler and linker automatically make all of the needed files. This is particularly important with C++ where calling conventions are mangled with function names during the decoration process.


Debugging

Debugging commands in the Alpha edition of the compiler work essentially the same as those in the x86 edition. The four areas that differ are the Register window, Disassembly window, symbol addresses, and Breakpoint command.


The Register Window

The principal difference in the debugger is in the appearance of the Register window. The procedure for viewing the Register window is the same as for the x86 edition: while debugging a running program, choose Registers from the Debug menu (or press ALT+5).

Registers on Alpha are not the same as on x86. Additionally, the Program Status Register (psr) is equivalent to the register window flags used in the x86 edition.

Alpha processors have 32 general integer registers, each of which is 64 bits wide, and 32 floating-point registers, each of which is 64 bits wide.

The following figure shows a typical display of the Register window when running on an Alpha platform.

Figure 2.7 The Alpha Register Window


The Disassembly Window

Disassembled code appears different on Alpha platforms. The procedure for viewing the Disassembly window is the same as for the x86 edition: while debugging a running program, choose Disassembly from the Debug menu (or press ALT+8).

The following figure shows sample output for the Disassembly window, with the Code Bytes option turned on. You can turn this option on or off by choosing Options from the Tools menu, and then selecting the Debug tab. Note that unlike machine code for x86 processors, each Alpha instruction is 32 bits.

Figure 2.8 The Disassembly Window on the Alpha Platform

Note Because of the code scheduling that takes place when you apply code optimizations, optimized code can have a very scrambled instruction order. Use the /FAcs compiler switch to make an annotated .COD file, which can help you debug optimized code.


Symbol Addresses

The architecture of a Alpha processor affects local variable symbol addresses, which you see in disassembly mode and in the Watch window. Be aware of the following regarding symbol addresses on Alpha platforms:

  • With Alpha processors, local symbols are usually accessed as offsets from the sp (stack pointer) register, rather than as offsets from the EBP (frame pointer) register as they are on x86 processors running in 32-bit mode. Normally, the symbol names are displayed and the stack pointer offset is hidden. Note that sometimes fp is used, and that there is no EBP on the Alpha platform.

  • If you have set the /Gt compiler option to /Gt0 as recommended, then global symbols smaller than 32 bytes are accessed as offsets from the gp (global pointer) register for faster access. Addresses may, therefore, be displayed as an offset from gp.

    See The /Gt (Set Data Value) Option in Chapter 4 for a detailed discussion about the /Gt switch. See the Alpha Calling Standard for details on when the stack pointer and frame pointer are used.

  • On the Alpha platform, when code uses C++ exception handling, structured exception handling, or _ _alloca, local symbols might be accessed as offsets from a frame pointer register (fp).

The following figure shows an example of the Locals window when running on a Alpha platform. Note that each address is an offset from SP, not EBP.

Figure 2.9 The Locals Window on the Alpha Platform


The Breakpoint Command

With the Alpha edition of Visual C++, breakpoints are set as they are for the x86 edition, but the effects of some breakpoints may differ.

Setting a breakpoint on a condition, with no location specified, can cause the debugger to execute code much more slowly with the Alpha edition. The breakpoint types that cause this situation are:

  • Break when expression is true

  • Break when expression has changed

Alpha processors do not support trace mode, so when one of these breakpoints is set, the debugger must evaluate the expression after every (emulated) machine-level instruction, even inside function calls. In some cases, the slow execution may even give the impression that the program has failed. However, the debugger is simply taking a long time to execute the code.

To speed up debugging in these cases, set a breakpoint without location only when you have narrowed a problem area to a small section of code, or set a series of breakpoints at specific locations - for example when entering routines.


Disabled Settings in the Development Environment

Although the Alpha edition of Visual C++ supports most project settings supported by the x86 edition, because of architectural differences it does not support all settings. Unsupported settings appear dimmed.

You can examine project settings by choosing Settings from the Project menu and then clicking a tab.

Disabled settings in the environment include the following:

  • Incremental compiling. In the Customize category of the C/C++ page in the Project Settings dialog box, the Enable Incremental Compilation check box is disabled.

  • Minimal rebuild. In the Customize category of the C/C++ page in the Project Settings dialog box, the Enable Minimal Rebuild check box is disabled.

  • Calling convention. For the Alpha platform, in the Code Generation category of the C/C++ tab in the Project Settings dialog box, only the _ _cdecl item is listed because this platform has only one calling convention.


Code Generation Settings

The Code Generation category for the project setting offers different choices for the target processor, depending on the target platform. You can view these settings by following these steps:

To View Code Generation Setting

  1. From the Project menu, choose Settings.

  2. Click the C/C++ tab.

  3. Select Code Generation from the Category list box.

The following figure shows the project settings displayed for the Code Generation category on the Alpha platform. Note the contents of the Processor list box.

Figure 2.10 Code Generation Category of the C/C++ Tab

The Processor list box contains the valid target processor choices for the current platform. With each Alpha edition of Visual C++, the default target processor is the same as the host type processor.

For more information on the processor choices for Alpha platforms, see the New Compiler Options in Chapters 5, Features Specific to Alpha.


Chapter 3 Porting Visual C++ Source Code

After creating a project and porting source files, the next step in moving code to the Alpha edition of Visual C++ is to make any required changes to the code. In many cases, the code will recompile and run without changes. This is especially likely if your source code uses structured, portable techniques. This chapter addresses specific issues in portability that are relevant when moving to the Alpha platform.

If you are not porting code from another platform, but creating entirely new projects on the Alpha platform, this chapter is useful in pointing out general guidelines for writing efficient C/C++ code that compiles without errors.

This chapter covers the following topics:

  • Overview of Portability Issues

  • Working with Data Alignment Issues

  • Accessing Procedure Arguments

  • Procedure Calling Convention on Alpha

  • Handling Variable Argument Lists

  • Using Format Specifiers


Overview of Portability Issues

The Visual C++ language is designed to be as platform-independent as possible. When portability issues arise, it is generally because the code makes erroneous assumptions about the underlying hardware.

C/C++ is a relatively low-level language. For example, given the address of an object on the stack, you can use pointers to attempt to access other objects you believe are also on the stack, just above or below the first object. Adopting such an approach is a typical cause of errors when porting code from x86 to Alpha because the Alpha calling conventions do not use the stack the same way that x86 calling conventions do.

Such conflicts are possible because the power and flexibility of C and C++ can sometimes be used to exploit implicit knowledge of how the underlying hardware operates. Unfortunately, such programming practices can cause problems that are difficult to debug when porting to other architectures. The following guidelines can help you in writing portable code:

  • To avoid alignment errors, turn structure packing off unless necessary; for example, when working with FAT file structures. If you must use structure packing, you should use the _ _unaligned keyword to avoid alignment fixups in memory. Fixing up alignment errors in memory can result in up to a 300% performance penalty.

  • Do not recast data pointers to a larger size and then dereference them; this is another cause of alignment errors.

  • Revise any code that attempts to use pointers to access arguments directly from the stack by using STDARG.H.

  • Check your use of format (e.g., printf and scanf) specifiers. The sizes of some types might change from one platform to another.

  • Use %Processor% in your makefiles instead of hard-coding or making assumptions about the processor architecture.


Using _ _setjmp

The Alpha editions of Visual C++ are especially sensitive to ANSI-compliant use of the _ _setjmp macro. There are two reasons for this: portability and performance. Only ANSI-compliant usage of _ _setjmp is truly portable across platforms, and noncompliant use can result in degraded application performance.

The American National Standards Institute (ANSI) standard explains that:

“..._ _setjmp be usable like any other function, im.e., that it be callable in any expression context, and that the expression evaluate correctly whether the return from _ _setjmp is direct or via a call to _ _longjmp.

Unfortunately, any implementation of _ _setjmp as a conventionally called function cannot know enough about the calling environment to save any temporary registers or dynamic stack locations used part way through an expression evaluation. (A _ _setjmp macro seems to help only if it expands to inline assembly code or a call to a special built-in function.) The temporaries may be correct on the initial call to _ _setjmp, but are not likely to be on any return initiated by a corresponding call to _ _longjmp. These considerations dictated the constraint that _ _setjmp be called only within fairly simple expressions, ones not likely to need temporary storage.

Other uses have undefined behavior and are not portable.”

This undefined behavior varies between the x86 and Alpha versions of Visual C++. An Alpha compiler may issue error or warning messages for non-ANSI uses of _ _setjmp that work without error with the x86 compiler.

As a result, on Alpha, ANSI-compliant use of _ _setjmp must be adhered to as described in the following excerpt from the ANSI standard:

“An invocation of the _ _setjmp macro shall appear only in one of the following contexts:

The entire controlling expression of a selection or iteration statement.

One operand of a relational or equality operator with the other operand an integral constant expression, with the resulting expression being the entire controlling expression of a selection or iteration statement.

The operand of a unary ! operator with the resulting expression being the entire controlling expression of a selection or iteration statement.

The entire expression of an expression statement (possibly cast to void).”


Using _ _setjmp and _ _longjmp with Structured Exception Handling

Note setjmp and longjmp are ANSI macros that Visual C++ expands to
_ _setjmp and _ _longjmp.

If you write code that uses both _ _setjmp and _ _longjmp and also uses structured exception handling, then you must include the #include module named SETJMPEX.H in the compilations of your source code. Including SETJMPEX.H in these situations causes the Alpha Visual C++ compiler to generate additional code for invocations of _ _setjmp and _ _longjmp that saves and restores the internal context required to interoperate properly with C++ structured exception handling. In addition, during _ _longjmp processing, all finally actions, exception filters, and catch handlers are invoked before control is transferred to the site of the associated _ _setjmp call. Upon return to the site of the _ _setjmp, all local variables reflect their updated values.

The additional processing comes at a price in performance of _ _longjmp. This performance degradation can vary widely depending on the platform and application. In general, if you do not require the coexistence of _ _setjmp and _ _longjmp and structured exception handling in your code, you should not include SETJMPEX.H in your compilations.


Working with Data Alignment Issues

As discussed in Chapter 1, Product Overview, alignment of data is an important issue when porting or writing code on the Alpha platform. Depending on the particular Alpha processor platform on which your application is run, unaligned operations with primitive data types can cause poor application performance, or cause your application to fault and terminate abnormally.

This section begins by presenting an overview of alignment and packing structures. Then, it discusses the different platform defaults regarding unaligned data operation behavior, how to change those defaults, suggestions for how to avoid alignment errors, rules for alignment of complex data types, the Microsoft Foundation Class Library (MFC), and solving alignment problems.


What Is Alignment?

Simple data types declared as variables are assigned addresses that are multiples of the size (in bytes) of the data type. Thus, variables of type long will be assigned addresses that are multiples of 4 (the bottom 2 bits of the address are 0).

Structures are padded so that each element of the structure will end up at a naturally aligned address. For example:

struct x_
{
   char a;     // 1 byte
   int b;      // 4 bytes
   short c;    // 2 bytes
   char d;     // 1 byte
} foo;

will actually be laid in memory so that it looks like this:

struct x_
{
   char a;            // 1 byte
   char _pad0[3];     // padding to put 'b' on 4-byte boundary
   int b;            // 4 bytes
   short c;          // 2 bytes
   char d;           // 1 byte
   char _pad1[1];    // padding to make sizeof(x_) multiple of 4
}

The result in both cases is that sizeof(struct x_) will return 12. Note that char _pad0[3] is included so that the int b member will be on a 4-byte boundary. The reason char _pad1[1] is included at the end is so that arrays of this structure will all have the same alignment; i.e.:

struct _x bar[3];

so that this array can be laid out as follows:

adr
offset   element
------   -------
0x0000   char a;         // bar[0]
0x0001   char pad0[3];
0x0004   int b;
0x0008   short c;
0x000a   char d;
0x000b   char _pad1[1];

0x000c   char a;         // bar[1]
0x000d   char _pad0[3];
0x0010   int b;
0x0014   short c;
0x0016   char d;
0x0017   char _pad1[1];

0x0018   char a;         // bar[2]
0x0019   char _pad0[3];
0x001c   int b;
0x0020   short c;
0x0022   char d;
0x0023   char _pad1[1];

This is important so that each member of each array element can be accessed naturally.


Alignment Issues on Alpha

The Alpha edition of Visual C++ aligns all data types on natural boundaries unless you use structure packing or use recast pointers to move data to an unaligned address. Natural boundaries are, for example, a longword on a longword boundary, and a quadword on a quadword boundary.

Though these systems can automatically fix alignment errors in software, application performance degradation as a result of unaligned data operations can be significant. This is why you need to avoid alignment errors when porting code to Alpha platforms.

By default, the alignment rules for Alpha platforms place data at an address that is a multiple of the size of the type. For example, a 2-byte data type (short) must be placed on an address that is a multiple of two. A 4-byte data type (long) must be placed on an address that is a multiple of four. This is often referred to as “natural” alignment. Note that nonprimitive data types align to the largest of their member’s native types.

Thus, a primitive data type of size 2n bytes must have an address with the n least-significant bits set to 0. These rules imply that the following types have the alignment shown:

Primitive data type | Alignment
char | byte
short | WORD (2 bytes)
int (equivalent to long on Windows NT) | DWORD (4 bytes)
long | DWORD
_ _int64 | QUADWORD (8 bytes)
float | DWORD
double | QUADWORD
Any pointer type | DWORD
_ _ptr64 | QUADWORD
Structures, unions, and arrays | The largest native member’s alignment requirement, possibly modified by /Zp or #pragma pack.


Avoiding Alignment Errors

In general, alignment errors can be avoided by following these rules:

  • Do not enable structure packing.

  • Do not access a small-aligned address by using a recast pointer of larger alignment. For example, treating the address of a char data type as a pointer to a long can cause an alignment error because this might mean executing a 4-byte move from an address that is not a multiple of four. The following example illustrates this style of coding:
    char a[10];
char *p = &a[1];
long l = *(long *)p;   // ERROR!; Attempt to move a long from
                       // the address of a char.

However, the converse operation, accessing a large-aligned address with a recast pointer of smaller alignment, is perfectly safe. For example, you could use a

(char *) cast to access the first byte, or any byte, of a long variable.

  •
If you do need to pack structures or move data to unaligned addresses, use the _ _unaligned keyword. Note, however, that this keyword cannot resolve alignment problems of pre-existing classes, for example, in inherited code, or the Microsoft Foundation Class Library.
    •

In programs that use large arrays of structures, it may be advantageous to use packed structures to conserve space, or you may be required to use structure packing to read a pre-existing data format. In such cases, you can still use packed structures, while carefully “unpacking” members of a structure before using data in the program. This technique may involve copying the data in a structure member by member, or element by element, or field by field, into a temporary location that is correctly aligned.

Alpha versions of the compiler automate safe access to unaligned data by providing the _ _unaligned keyword. If you are writing code meant to compile in both Alpha and x86 environments, you should define _ _unaligned as UNALIGNED because headers are designed to remove the _ _unaligned keyword from x86 builds. For example:

if defined (_M_ALPHA)||defined(_M_MRX000)||defined(_M_PPC)
   #define UNALIGNED __unaligned
#else
   #define UNALIGNED   // NULL
#endif

Syntactically, UNALIGNED (or _ _unaligned) is a type qualifier like const and volatile; it controls the meaning of what the pointer points to. However, _ _unaligned has meaning only when used in a pointer declaration. Some sample UNALIGNED pointer declarations are shown in the following example:

#define UNALIGNED __unaligned

UNALIGNED int *p1;               // p1 is a pointer to unaligned int
UNALIGNED struct {int i;} *p2;   // p2 is a pointer to unaligned struct
UNALIGNED int *) q;              // cast q to an unaligned int*

As an example of use, suppose that p1 is a pointer to unaligned int, as above, and that the packsize is set to 1. A packsize of 1 means that each member of a structure is aligned on the next available byte, regardless of alignment considerations. The following code illustrates the correct and incorrect use of _ _unaligned and #pragma pack in conjunction with integer operations. In the first section, a fault is generated because the _ _unaligned qualifier is not used, whereas in the second section, the _ _unaligned qualifier is used correctly.

#pragma pack (1)
struct s {
   char c;                    // offset 0
   int i;                     // offset 1!
} ss;
#pragma pack ()

void f(int *p)
{
   *p = 23;                   // will generate a fault
}

void g(_ _unaligned int *p)   // OK
{
   *p = 42;
}

void main ()
{
   f(&ss.i);
   g(&ss.i);
}

The following example shows the machine code output from the sample above. Function f shows the code generated by the improper handling of unaligned data, and function g shows the extra code generated when _ _unaligned is used.

Note that in function g, even though approximately 18 extra instructions are generated to handle the unaligned data, that code will execute much faster than if the operating system is left to perform the alignment fixup, as in function f.

f::
   mov 23, t0
   stl t0, (a0)      // This instruction will get an alignment fault.
   ret ra            // 300X slowdown
   nop

g::
   and a0, 3, t4
   mov 42, t0
   bne t4, L$3
   stl t0, (a0)
   ret ra
   L$3:
   bic a0, 3, t2     // Code to safely handle an unaligned address.
   and a0, 3, t5     // 3X slowdown.This is much faster than letting
   ldl t3, (t2)      // the OS handle it
   insll t0, t5, t4
   mskll t3, t5, t3
   addq t5, 4, t5
   bis t3, t4, t3
   ldl t4, 4(t2)
   stl t3, (t2)
   inslh t0, t5, t3
   msklh t4, t5, t4
   bis t4, t3, t4
   stl t4, 4(t2)
   ret ra               ; 000016

There is a performance penalty for accessing data through an _ _unaligned pointer. To guarantee that there are no alignment errors, the compiler must access the dereferenced data as a series of smaller pieces. If the data is already properly aligned, this technique for accessing data is not necessary. Therefore, use the _ _unaligned keyword only when needed.

Packing Structures

The problem here is that the structure actually requires 12 bytes, when the programmer may have intended it to take up only 8 bytes. If space requirements are critical, you can reorder the members of the structure so that the same-sized elements are next to each other and pack tightly. Usually, you start with the largest members and work your way down to the smallest ones. The previous example could be reorganized as:

struct x_
{
   int b;     // 4 bytes
   short c;   // 2 bytes
   char d;    // 1 byte
   char a;    // 1 byte
} foo;

Now all of the members end up on naturally aligned boundaries, and the actual size of the structure is 8 bytes instead of 12.

There are two problems with this approach. The first problem is that reordering assumes that the user has full control over how the data structure is defined and laid out. If, for example, the data structure represents the layout of fields in a file on disk, the user may not have the freedom to rearrange the members. Yet in order to read the data from disk efficiently, the user would like to have no padding between the members.

The other problem occurs where the structure size requires padding at the end to make array elements have the same alignment, and the user needs to make sure that there is no padding between the array elements (either for memory savings or, again, for reading data from a fixed-format source). Here the use of the compiler directive #pragma pack(1) may be necessary. This will, however, cause elements of the structures to be unaligned, and it will require the use of the _ _unaligned qualifier on Alpha to generate the special code to access this data without taking alignment traps to the operating system. For example:

# pragma pack (1)
struct x_
{
   char a;    // 1 byte
   int b;     // 4 bytes
   short c;   // 2 bytes
} foo;
# pragma pack ()

void bar()
{
   __unaligned struct x_ *px = &foo;
   . . . .
   px->b = 5;
}

This code tells the compiler that the pointer px points to data that is not naturally aligned, and tells the compiler to generate the appropriate sequence of load/merge/store operations to do the assignment efficiently.

Note that _ _unaligned should only be used as a last resort, because the generated code is less efficient than accessing naturally aligned data. However, _ _unaligned is preferable to the performance penalty incurred with alignment faults.

If at all possible, arrange the members of a data structure to preserve alignment and minimize space at the same time, as outlined above.


Rules for Alignment of Complex Types

All data types are either primitive types, which are listed in Avoiding Alignment Errors, or arrays, structures, or unions of simpler types. The following list summarizes the rules for alignment of complex types:

  • The alignment of an array is the same as the alignment of the base type.

  • The alignment of a structure (or object) is the maximum of the alignment factors of all members of the structure. Furthermore, as long as structure packing is turned off, the compiler pads the structure so that each member is placed at the next available properly aligned address for that member.

  • The alignment of a union is also the maximum of the alignment factors of all members. Each member’s address is that of the union itself, so there is no padding.


Effect of Structure Packing on Alignment

As long as the alignment rules discussed in the previous section, Rules for Alignment of Complex Types, are observed, no alignment errors occur. Structure packing, however, interacts with compiler alignment behavior as follows.

For each member of a structure:

  • If the packsize is set equal to or greater than the default alignment, the packsize is ignored.

  • If the packsize is set smaller than the default alignment, the compiler aligns according to the packsize value.

Thus, if the packsize is set to 4, data types having a size of 4, 8, or 16 bytes would be aligned on addresses that are multiples of four. However, there is no guarantee that data types 8 bytes in size (64 bits) would be aligned on addresses that are a multiple of eight. The packsize has no effect on data types outside of a structure.

The following techniques set a packsize, in bytes:

  • The command-line option /Zp n sets the packsize to n, in which n can be 1, 2, 4, 8, or 16, and in which 8 is the default.

  • The compiler directive #pragma pack([n]) sets the packsize to n, in which n can be 1, 2, 4, 8, or 16. If n is not specified, #pragma pack resets the packsize to its value at the beginning of compilation: either the value specified by /Zp, or the default, which uses no packsize. The pragma applies only from the point at which it occurs in the source.

    The syntax of the #pragma pack directive supports other features for optionally saving and restoring the current packing state. For more information, see “Pack” in the Preprocessor Reference.

For example, the /Zp1 option sets the packsize to 1, which causes the compiler to use no padding within structures. Consequently, any data type that has an alignment factor larger than 1 may cause an alignment fixup to occur when accessed. To avoid this problem, either turn off structure packing or use the _ _unaligned keyword as described in Alignment Issues on Alpha.


Unaligned Data Behavior on Alpha

The Alpha platform's default behavior when it encounters an unaligned data operation involving a primitive data is for the operating system to fix up the alignment fault in memory. This default behavior can be changed system-wide or on a per-process basis.

The following table summarizes platform behavior and how to change the default behavior.

Default system behavior when aligned data operation is encountered | To change globally | To change on a per-process basis
Fixed up in memory by Windows NT. | Run axpalign utility with the /enable or /disable parameter and reboot. | Use SetErrorMode (SEM_NOALIGNMENTFAULTEXCEPT);


Allowing Alignment Errors

As the previous table indicates, the Alpha platform's default is for Windows NT to fix up alignment errors in memory system-wide, and to provide a mechanism for changing this behavior both system-wide and on a per-process basis.

The Alpha platform designers decided that it was more important to support unmodifiable pre-existing code that performs unaligned data operations, and not require users to wait for upgrades to their applications. For developers working on Alpha, more diligence in the pursuit of avoiding unaligned data operations is required.

While there are clear pros and cons to each approach, it is agreed upon by all platform designers that developers should rely on unaligned data operations only when absolutely necessary. It is best to avoid fixups by writing tight code.

Consequently, developers are encouraged to run their development systems with axpalign /enable if at all possible. This helps expose alignment faults as early as possible during the development process. In addition, application testing should be performed after having run axpalign /enable to ensure that unintended fixups are not occurring.


Disabling Automatic System-Wide Fixing of Alignment Errors

To disable automatic fixing of alignment errors, the default operating system control for alignment exceptions must be changed so that alignment faults become visible to your application. You can then use any just-in-time debugger to locate the source of the alignment faults.

You can change the operating system default either in the registry with REGEDT32 or by using the axpalign command. To enable alignment fault exceptions, type the following at the command-line prompt and reboot your system:

axpalign /enable

To disable alignment fault exceptions, type the following at the command-line prompt and reboot your system:

axpalign /disable

Note This change is system-wide and affects all applications. Older applications that still contain alignment errors may terminate with data misalignment exceptions. Therefore, you should use only the application you are debugging and testing when axpalign is enabled. Failures of other software when using axpalign should be reported as bugs to the appropriate maintainers.


Enabling Automatic Fixing of Alignment Errors on a Per-Process Basis

If your application requires Windows NT to handle alignment faults for a particular process regardless of the system-wide default, insert the following statement early in your program:

SetErrorMode(SEM_NOALIGNMENTFAULTEXCEPT);

This statement must be executed before any alignment error occurs. The effect of the statement is to set a flag that causes the operating system to handle alignment errors. Each alignment error causes a hardware exception, which is then handled through the operating environment’s exception-handling routines. Programs continue, but are slowed down.

Note that this technique does not work if the application is run on a version of Windows NT prior to 3.5. Also note that on Alpha, using this statement has no effect if automatic handling of alignment errors has previously been enabled system-wide by using the axpalign /disable command.

It is important to save or restore the old state before and after calling into an external dynamic-link library (DLL). This may change the error mode and leave it in either state. Call SetErrorMode(0) to get the state.


Accessing Procedure Arguments

Because Alpha processors handle procedure arguments differently than other processors, relying on assumptions about how arguments are stored in memory can cause serious compatibility problems. To avoid incompatibility, do not rely on pointer arithmetic that accesses arguments by using an offset from the beginning of the argument list. Programs that have variable argument lists sometimes use this technique to access individual arguments. Such programs are inherently nonportable. You can avoid these problems by including STDARG.H.


Procedure Calling Convention on Alpha

The Alpha version of the compiler uses a different procedure calling convention than the x86 version. The Alpha calling convention is a register-based convention that frequently uses argument registers in cases where the x86 version of the compiler would place all arguments on a stack frame. There are six integer argument registers and six floating-point argument registers on Alpha. Consequently, code that assumes that an argument is on the stack is likely to fail. Additionally, note that using a unified calling standard precludes the use of _ _declspec (naked).

All differences in the calling conventions are resolved at the C/C++ source code level, as long as you do not make assumptions about how arguments are located on the stack or in memory. Code written to optimize handling of variable argument lists sometimes makes such assumptions by using direct manipulation through pointers. The section Handling Variable Argument Lists discusses safe, portable approaches to variable argument lists.


Obtaining the Alpha Calling Convention

For more information on the Alpha calling convention, see the Windows NT for Alpha Calling Standard, which is included in the Visual C++ book set on your MSDN CD-ROM. Also invaluable is the Alpha Architecture Reference Manual, 3rd Edition, written by Digital Press and published by Butterworth-Heinemann. The ISBN number is 1-55558-202-8.


Handling Variable Argument Lists

If your program uses variable argument lists, make sure you access arguments by using the ANSI-compatible macros defined in STDARG.H:

va_arg
va_end
va_list
va_start

Use of these macros enables the same source code to correctly access arguments, regardless of the platform.

Note that you can use VARARGS.H with existing code, but because STDARGS.H is part of the ANSI C standard, it should be used for new code.


Using Format Specifiers

As discussed previously in this chapter, alignment issues are more significant on Alpha platforms than on the x86 platform. For example, it is important to use caution when using format specifiers with function families such as scanf and printf. For example, in the case of scanf, moving a dword (32-bit word) to an address that is word (16-bit) aligned can cause an alignment error. It will also overwrite adjacent storage. Therefore, a%d (dword) read to a 16-bit field is unsafe. However, reading a word into a dword field is generally safe.


scanf Format Specifiers

The following tables detail the data size read from a file or string when used with floating-point and integer scanf format specifiers:

Floating-point format specifier | Data read from file or string
%l{e|f|g} | quadword (64 bits)
%{e|f|g} | dword (32 bits)

Integer format specifier | Data read from file or string
%I64{d|i|o|u|x} | quadword (64 bits)
%{d|i|o|u|x} | dword
%h{d|i|o|u|x} | word (16 bits)
%c | byte


Floating-Point Denormals

All Windows NT platforms use identical IEEE floating-point formats. The Alpha platform handles finite floating-point values in the same way as x86, but it handles nonfinite floating-point values differently.

By default, the Alpha compiler generates code that raises an imprecise floating-point exception when nonfinite floating-point values are encountered or when division by zero or overflow occurs. The instruction that raised the exception might be located prior to where the trap occurred. The status code of the exception will be one of the following:

Status_Float_Divide_By_Zero

Status_Float_Invalid_Operation

Status_Float_Overflow

Status_Float_Underflow

Status_Float_Inexact_Result

The default behavior on x86 is to create an IEEE masked result, set a bit in the fspr, and for the hardware to raise no exception. Programs that require the default nonfinite x86 floating-point behavior must:

  • Be compiled with the /QAieee switch or

  • Call _controlfp(_DN_SAVE, _MCW_DN)

Note that _controlfp(_DN_SAVE, _MCW_DN)is optional. The /QAieee switch will cause the compiler to generate code so that the RTL and operating system can find the faulting floating-point instruction. The _controlfp function tells the operating system that it must maintain denormal values instead of the default behavior, which is to turn them into zeroes. The _controlfp function is only important to use in applications that require the extra bits of precision to work. A better solution is to use numerically stable algorithms.

If one of the exceptions listed above is raised, the location of the exception may not be where the trap occurred. From the debugger, look for the exception at the instructions preceding the error. If you are still having trouble locating the exception, the /QApe switch might help.


LARGE_INTEGER Data Type

Windows NT defines a LARGE_INTEGER data type that is a 64-bit integer created from an array of two longwords. A LARGE_INTEGER type is not the same as a quadword. The quadword is a true, nonportable, 64-bit integer data type; the Windows NT LARGE_INTEGER is a data type that is 8 bytes in size and is usable as a 64-bit integer only in conjunction with a set of run-time library functions. You should not redefine the LARGE_INTEGER type.


CONTEXT Data Structure

The CONTEXT data structure is architecture-dependent, and code that accesses this structure may have to be modified when ported to the Alpha platform.


Page-Size Assumptions

The page size for Windows NT is architecture- dependent. For the Alpha platform, the default page size is variable, with a minimum page size of 8K.

For portability, application code should not depend on a hard-coded page size. If an application assumes a particular page size, it will not work correctly on these systems. If you use the /base:nn linker option, be sure to align to at least the default page size for your platform.


Multi-Threaded Granularity of Memory Access

The Alpha platform allows multithreaded access to shared data structures. Because of the potential for unintended writes to shared data structures, good programming practices make it essential that these structures be protected from writes with locks (for example, EnterCriticalSection, LeaveCriticalSection). This should be observed when both the alignment and size of the shared data match an atomic load/store unit of the underlying machine architecture, and even more critically, should especially be observed when the shared data size does not match a natural boundary or is smaller than 4 bytes.

For Alpha systems, granularity is 4 or 8 bytes. For x86 systems, it is 1, 2, or 4 bytes. For portability across these platforms, you should assume 4-byte granularity or protect multithreaded access to shared data with locks.


Jump Buffer

The jump buffer is platform- and compiler-specific. If you use the jump buffer, do not link objects produced on different platforms or by different compiler versions.


Pragmas

There are no Alpha-specific pragmas. The Alpha edition of Visual C++ supports the following subset of Microsoft C pragmas:

alloc_text | hdrstop | setlocale
comment | intrinsic | warning
data_seg | message |
function | pack |

The Alpha edition of Visual C++ recognizes but ignores the following Microsoft C pragmas:

auto_inline | inline_recursion | page
check_stack | linesize | pagesize
code_seg | native_caller | subtitle
inline_depth | optimize | title

The Alpha edition of Visual C++ does not recognize the following Microsoft C pragmas, and their use causes a warning message to be issued:

check_pointer

skip


Integer Division by Zero

By default, integer division by zero is reported as an exception on Alpha platforms, although not on x86. This behavior is an aid to locating and debugging integer-division-by-zero errors.

On the 21064 and 21164 processors, you get an imprecise exception and might need to use the /QAieee option to help locate the divide that caused the fault. The /QApe compiler option might also be helpful in debugging HPARITH faults.


Chapter 4 Using Command-Line Compiler and Linker Options

When porting an external makefile (one not created in the Visual C++ development environment) to a Alpha platform, you should first scan your file to remove unsupported options and make any other necessary changes. This chapter describes how to change your makefiles.

These guidelines also apply if you use the CL or LINK utilities directly from a command window.

If you work exclusively in the Visual C++ development environment, then you generally do not need to worry about command-line options. The environment automatically adopts options that are supported by the platform.

This chapter covers the following topics:

  • Testing the Processor Type in Makefiles

  • The CL Command Line

  • Linking Objects_Built with the /Gt Switch

  • Module-Definition Files


Testing the Processor Type in Makefiles

Windows NT defines the environment variable PROCESSOR_ARCHITECTURE on all platforms. On x86 platforms, this string is defined as “x86”. On the Alpha platform, this string is defined as “ALPHA”.

You can use the !IF and !ELSEIF conditionals in your makefiles to execute different actions depending on the value of the PROCESSOR_ARCHITECTURE string. For example:

!IF "$(PROCESSOR_ARCHITECTURE)" == "ALPHA"
#   Execute Alpha-specific actions

!ELSEIF "$(PROCESSOR_ARCHITECTURE)" == "x86"
#   Execute x86-x86-specific actions

!ELSE
#   Exit with error message
!ENDIF

For additional related information, see the Overview of Portability Issues topic in Chapter 3.


The CL Command Line

The sets of command-line options for the Alpha and x86 editions of the compiler are almost identical. However, the x86 edition supports some options that the Alpha edition does not. Make sure to remove these unsupported options from your makefiles. Some x86 options are supported on Alpha platforms, but with changes. Make sure you use these options correctly in makefiles.

In addition, the Alpha editions support some options that the x86 edition does not. The next three chapters describe these platform-specific options. When porting from x86, however, you only need to worry about the x86 options that are not supported.

Tip For best results on applications with a lot of global data, use the /Gt option, which sets the size threshold for near data, when porting applications to the Alpha platform. Note that you should use /Gt32 with caution and only when necessary as its use can result in complications such as linker errors or disabling of future optimizations. For more information on the use of the /Gt switch, see The /Gt (Set Data Value) Option topic later in this chapter.


Unsupported Options

The Alpha edition of Visual C++ supports the same command-line options as the x86 edition, with the exception of some code-generation options. These options fall into three groups: processor targeting, calling conventions, and optimization.

The following x86-specific processor targeting options are accepted, but ignored:

Option | Description
/G3 | Optimize for 80386
/G4 | Optimize for 80486
/G5 | Optimize for Pentium

See the Options for Maximum Speed section for information on switches to target your application for specific Alpha processor families.

The Alpha edition of Visual C++ recognizes only the Alpha calling convention. This cannot be modified, overridden, or disabled.

The following table summarizes support for compiler options on Alpha platforms. If an option is unsupported on your platform, it should be removed from makefiles.

Option | Description | Alpha
/Oa | Assume no aliasing | Supported
/Ow | Assume aliasing across function calls | Supported
/Og | Enable global optimization | Ignored
/Op | Preserve floating-point accuracy | Supported
/Op– | Allow loss of low bit(s) of floating-point calculations | Supported
/Ot | Specify fast sequences of machine code | Supported
/Oy | Omit frame pointer in the program stack | Ignored
/Oy– | Generate frame pointer in the program stack | Supported
/Gd | Use the _ _cdecl calling convention | Ignored
/Gr | Use the _ _fastcall calling convention | Ignored
/Gz | Use the _ _stdcall calling convention | Ignored
/Gs | Disable stack checking | Supported
/Ge | Enable stack checking | Supported

You can use PROCESSOR_ARCHITECTURE to maintain the same makefile for both platforms, as described in the section Testing the Processor Type in Makefiles. Disable the x86-specific options when the value of this string is not “x86”.


Optimization

The Alpha edition of Visual C++ provides four basic levels of optimization:

Option | Description
/Od | Perform no optimization.
/O1 or /Os | Perform all optimizations that are consistent with keeping the resulting code small.
/O2 or /O | Perform all speed optimizations. This option implies /Ob3 and /Oi, but you can override these defaults.
/Ox | Perform optimizations that favor speed over code size. This option implies /Ob3 and /Oi, but you can override these defaults.

If you do not specify an optimization option, the default is /Ox, which includes /Gf, /Gy, /Ob3, and /Oi.

Note If you are porting a project from the x86 platform, see the Reset Project Options topic in Chapter 2 for a discussion of cross-platform optimization issues.


Options for Maximum Speed

The execution speed of generated code varies depending on the options specified at the command line. The table below, and the following sections, shows the options that produce the fastest running code on Alpha systems. Note that these are suggested options. A particular program may need to consider other factors, such as code size, that make different choices more suitable. You should consider the effects of each option, as well as its requirements, before deciding how to build applications for the best performance on Alpha systems.

Option | Description
/Oi | Enable intrinsic functions; enabled automatically for /Ox and /O2, but not for /O1.
/O2 | Perform all speed optimizations. This option usually produces faster code than /Ox, but sometimes results in much larger code size. Compare the size and speed of an application compiled with /O2 to its size and speed when compiled with /Ox before deciding which options to use.
/J | Change default char type to unsigned. This option can produce faster code in applications that frequently manipulate values of type char. Note, however, that some applications may depend on a default setting that treats char as signed in order to operate correctly.
/Op– | Override floating-point precision in favor of faster code. Using this option may produce slightly different results than would be produced by following the ANSI C rules for expression evaluation. This is the default except when /Za is used.
/QAgq | Use quadword granularity. This option produces faster and smaller code when used with single-threaded applications. For more information, see the section /QAgq Option in Chapter 5.
/Ob[0 1 2 3 4] | Procedure inlining optimization. /Ob0 disables procedure inlining optimization. /Ob1 only inlines small procedures marked with _ _inline. /Ob2 enables automatic inlining, where the compiler balances code size and speed. /Ob3 favors speed over code size. /Ob4, also called benchmark inlining, tries to inline all calls, resulting in fastest possible performance but larger code size. Depending on the size and complexity of your application, compilation times can become quite long when using /Ob4.
/QAieee0 | Do not support IEEE floating-point NaNs, infinites, and denormals in the compiled code. This is the default. For more information, see the section /QAieee0 Option in Chapter 5.
/GF | Pool strings and place them in read-only storage. Allows the compiler to schedule loads of the string literals earlier. This can help in string-bashing codes, and reveals many coding errors with access violations.


Processor Optimization Options

The Alpha edition of Visual C++ provides a family of switches that allow you to optimize compiled code for a specific processor within a platform. Processor optimizations are accomplished by a combination of code pattern selection, code scheduling, and instruction selection. Using a processor-specific optimization switch can result in code that will not run on a different processor.

If you do not want to optimize your code for a specific processor, do not use any of these switches. The compiler will then use a blended model, resulting in code that executes well across all processors within a platform family.

For Alpha processors the compiler accepts:

Processor switch | Description
/QA21064 | EV4
/QA21164 | EV5
/QA21164A | EV56 with byte/word support
/QA21164PC | EV56 with byte/word support and Motion Video Instructions (by _ _asm)
/QA21264 | EV6 with byte/word support, Motion Video Instructions, and new core integer and floating-point instructions

Important Using a processor optimization switch that enables byte/word support produces code that will not run on previous processors without byte/word support.


The /Gt (Set Data Value) Option

The /Gt option works differently on Alpha platforms than on x86. It sets the size threshold for small data. Although Alpha processors do not use the same concept of segment architecture as 16-bit Windows, the Alpha edition of Visual C++ does use the /Gt option to determine which data items to access with the GP register. The reason for this is that offsets in memory instructions are only 16 bits long. Therefore, ordinary load and store operations take ldah_H^addr() and ldl_L^addr(). With /Gt, small data is placed in .Sdata instead, and only one instruction is needed.

Using /Gt to Optimize Data Allocation

The Alpha edition of Visual C++ uses the /Gt option to control how data items are accessed through registers. You use this option to optimally allocate data for an application. For best results, you should add the /Gt option to external makefiles that produce applications. As mentioned in Testing the Processor Type in Makefiles, however, you may need to test the PROCESSOR_ARCHITECTURE string in makefiles to ensure that /Gt is used only on Alpha platforms. The /Gt option is not supported for 32-bit applications running on the x86 platform.

The following list provides guidelines for using the /Gt option:

  • Use the /Gt32 option when building applications.

  • Use the /Gt0 option (the default) when building DLLs. This use is required for DLLs.

Alpha processors use a GP (Global Pointer) register to efficiently access frequently used memory. Data in the GP area can be accessed with one machine instruction rather than two. Because the GP register is used in conjunction with a 16-bit offset, the area of memory that can be accessed through the GP register is 32K. With their flat address space, systems using the Alpha processor do not have segmented memory. However, the area of memory accessed through GP is similar to the concept of a near data segment and is also called the “small data area” or “fast data area.”

On Alpha platforms, the /Gt option controls how data is placed into the area accessed through the GP register; for a given setting /Gtn, objects smaller than n are placed into this fast data area. If you get linker errors, try a smaller /Gt value.

Important Use of /Gt32 requires that every object to which your application links must also use /Gt32, except for external library code that doesn't have externally defined data references. If you are building code that will be used as others as a library or .DLL, build with /Gt0 unless you are sure that anything calling your code is also built with /Gt32.

If you are linking manually, see the Linking Objects_Built with the /Gt Switch topic in this chapter.


The /Gf and /GF Options

By default, optimization levels /O1 and /O2 include /Gf, which allows for writable string pooling. As an optimization strategy, string pooling provides object and executable code size benefits. Writable string pooling, however, can be problematic because it is easy to unintentionally modify strings. When writing large programs, debugging this type of error can be difficult.

It is suggested that, where possible, you use /GF instead of /Gf. The /GF switch enables string pooling, but makes the string pool read-only. Similarly, if you use an optimization level that includes /Gf, you can override this switch by manually adding /GF to the compiler command line. Besides code size benefits, /GF also provides performance benefits.

Note that if you use /GF and attempt to write to a read-only string, the application will fail with an Access Violation error (ACCVIO).


The /Gs Option

The /Gs option is used to disable stack checking. To enable stack limit checking, use the /Ge option.

When stack limit checking is enabled, it is assumed that there may only be a single guard page for a thread. For the Alpha architecture, a hardware page can have a size of 8K, 16K, 32K, or 64K, depending on the implementation. The stack checking logic is based on the minimum allowed page size of 8K.

Because of architectural differences, /Gs is less useful on Alpha than on x86. For more information, refer to the Alpha Calling Standard and the Alpha Architecture Reference Manual.

Note If you disable stack checking, verify that your application only uses small frames, or that you’ve linked with a large enough initial stack size to satisfy program requirements. With stack checking disabled, overwriting the end of the stack can have unpredictable results.


The /Op and /Op- Options

The /Op and /Op– Options

The Alpha edition of Visual C++ supports both the /Op and /Op– options, which you use to improve or override floating-point precision. The /Op option disables arithmetic identities, so for example a+(b+c) will not be changed to (a+b)+c. This differentiation is only important with numerically unstable algorithms. On x86 platforms, the default is /Op–, which allows some loss of floating-point precision in favor of faster code. The Alpha default is /Op–.

Tip Because /Op– performs different optimizations across platforms, use /Op to ensure consistent results.


The /Fa and /FA Options

The Alpha edition of Visual C++ produces both assembly language source (.ASM) files and machine code (.COD) files.

The following table shows the available /Fa and /FA options:

Option | Description
/Fa | Treated as if /FAa specified
/Fa filename | Generates .ASM file
/FA | Treated as if /FAc specified
/FAa | Generates .ASM file
/FAc or /FAac | Create machine code (.COD) file
/FAcs or /FAacs | Create a combined source and machine code listing
/FAs or /FAas | Treated as if /FAcs specified

If you add /Fa or /Fa filename to any of the /FA options, the result only determines the name of the output file.


Linking Objects Built with the /Gt Switch

If you use CL both to compile and to link, then /Gt32 enables the optimal use of the GP register for most applications. However, if you link separately by using LINK, then the /Gt option must also be passed to the linker by using the /gpsize:n option. To preserve the effects of /Gt32, specify /gpsize:32 on the linker’s command line.

When developing and debugging your application, it is a good idea to use the incremental linking feature of Visual C++. When building your release product, however, disable incremental linking to increase performance.


Module-Definition Files

Avoid putting symbols in module-definition files that use decorated names. Name decoration schemes can differ depending on the compiler and the platform. In general, you should not rely on the way name decoration is used from one version to the next. Aside from this, you can use the same module-definition files on both platforms.

The use of _ _declspec(dllimport) and _ _declspec(dllexport) is strongly recommended as a means of achieving portability between platforms.


Chapter 5 Features Specific to Alpha

By following the guidelines in the previous chapters, you can write code that is fully portable between the Alpha and x86 platforms. It should be possible to write almost any kind of application without writing code specific to either x86 or Alpha. However, the Alpha version of the compiler provides many platform-specific features that are useful in special situations.

Many Alpha-specific features have been added to take advantage of added capabilities or to better optimize your program when compiling for an Alpha processor. If you want to write portable code, use these features with care, and conditionally compile them within #ifdef blocks so that you don’t attempt compilation of Alpha-specific features on other platforms.

This chapter covers the following topics:

  • Conditional Compiling

  • Alpha-Specific Compiler Options

  • Floating-Point Exception-Handling Options

  • Floating_Point_Rounding_Options

  • Keywords

  • Intrinsic Functions

  • Support for Microsoft C Intrinsics

  • Windows NT for Alpha Assembler

  • IEEE Floating-Point Behavior on Alpha


Conditional Compiling

Any of the following macros can be used to identify Alpha as the processor for which the compiler is generating code:

_ _M_ALPHA

_ _ALPHA_

_ _alpha

_ _ALPHA

The preferred macro is _ _M_ALPHA because it is consistent with the x86 version of Visual C++. The Alpha edition of Visual C++ predefines these macros, but the x86 edition does not. Therefore, you can use them with #ifdef to conditionally compile code for the Alpha edition of Visual C++ only. See Working with Data Alignment Issues in Chapter 3 for an example.

In makefiles, use the PROCESSOR_ARCHITECTURE environment variable to conditionally compile code. For more information, see Testing the Processor Type in Makefiles in Chapter 4.


Alpha-Specific Compiler Options

The Alpha version of the Visual C++ compiler includes several platform-specific compiler options. The /QAgl and /QAgq options control the level of granularity at which the compiler accesses data. The /QAieee n options control the behavior of nonfinite floating-point values. The /QAOu n options set loop unrolling.

/QAgl (Longword Granularity)

For multithreaded applications, the /QAgl option causes the compiler to access data in units of longwords (4 bytes of data on 4-byte address boundaries) as required by Windows NT. This is the default.

On Alpha systems, this may produce slightly less efficient code than quadword granularity.

/QAgq (Quadword Granularity)

For single-threaded applications, the /QAgq option allows the compiler to manipulate data in units of quadwords (8 bytes of data on 8-byte boundaries). If you know that a single-threaded application will have no data accessible to multiple threads of execution, use this option to produce the most efficient code on Alpha systems. Multithreaded applications may not operate correctly with this option.

/QAieee and /QAieee1

The /QAieee and /QAieee1 options are identical and specify that the compiled code supports IEEE floating-point NaNs, infinites, and denormals. Use one of these options for any application requiring typical IEEE floating-point exception behavior.

/QAieee0

The /QAieee0 option specifies that the compiled code does not support IEEE floating-point NaNs, infinites, and denormals. Underflows are quickly forced to zero, and the use of a NaN or Infinity raises an exception. This option is the default and should be used for all applications except those requiring IEEE-compliant floating-point exception behavior because it produces the fastest execution speed. Because run-time library routines may still produce NaNs and denormals, the use of _ _matherr is also recommended.

/QAieee2

The /QAieee2 option is the same as /QAieee1 but also enables IEEE inexact exceptions. Use this option only for the rare application that requires the IEEE inexact exception to be raised.

/QAOun

The /QAOu n option sets loop unrolling, where n specifies the number of iterations of the loop to unroll. If you set n to 0, the compiler chooses the number of iterations.

Note that the /Os compiler option causes /QAOu1, which disables loop unrolling.

/QAltls

The /QAltls option enables the compiler to generate extra code to reference data in thread-local storage when its size exceeds 32K. Enabling large thread-local storage exacts a moderate perfomance penalty on compiled code. Because of the extra code generated by the use of this switch, use it only when necessary. Linker error LNK2004 indicates that the /QAltls switch might be necessary.


Floating-Point Exception-Handling Options

The procedure descriptor for each function generated using the Alpha Visual C++ CL compiler contains data that instructs the Windows NT kernel and run-time libraries how to handle floating-point exceptions. Use of the following options allows you to control the contents at this area in the procedure descriptor. For more information on Procedure Descriptors, see the Alpha Calling Standard.

Most developers do not need to be concerned with these options, but developers implementing run-time libraries might wish to use them.

/Qapdca

Use the caller's exception mode. Math library functions are compiled in this mode so that IEEE exceptions may be fixed up in accordance with the user's code.

/QApdie

IEEE: Raise no exceptions except as controlled by separate IEEE exception-enabling bits; create infinite, denormal, and NaN values according to the IEEE floating-point standard.

/QApdsa

Signal All: Raise exceptions for all error conditions (including underflow).

/QApdsg

Signal: Raise exceptions for all error conditions except for underflow, which yields a zero result.

/QApdst

Silent: Raise no exceptions, create only finite values (no infinites, denormals, nor NaNs). In this mode, either the function result or the C language errno variable must be examined for any error indication.

The default is -QApdst unless -QAieee, -QAieee1, or -QAieee2 is set. If IEEE floating-point behavior is specified (-QAieee, -QAieee1, -QAieee2), /QApdie is used by default.

/QApe

Enables precise floating-point exceptions. Use of this switch slows down floating-point operations and is useful mainly when the floating-point exceptions are trapped at the instruction that generated the exception. This can be useful for debugging floating-point errors.


Floating-Point Rounding Options

On Alpha, each floating-point operation can specify the rounding mode used to generate the result of the operation. There are three IEEE rounding modes that can be set with a compiler switch: round to nearest, round to zero, and round to minus infinity. Use of these switches depends on the methods used by the application to control loss of precision.

In addition, there is a dynamic rounding mode available. Dynamic rounding mode allows the rounding mode to change while the program runs, resulting in a finer level of accuracy versus performance control. Round to plus infinity is only accessible through this switch.

Round to plus infinity and round to minus infinity run significantly slower on Alpha systems.

The default is always to round the result to the nearest floating-point value. Specialized floating-point applications may wish to use the following switches:

/QArc

Round to zero (or chop).

/QArd

Round dynamically. Rounding mode is set dynamically by calls to _ _controlfp. Round to plus infinity is accomplished by compiling with /QArd and calling _ _controlfp (_RC_UP, _MCW_RC).

/QArm

Round to minus infinity.

/QArn

Round to nearest value. This is the default rounding mode.


Keywords

The Alpha edition of the compiler defines three keywords: _ _unaligned, _ _int64, and _ _ptr64. To ensure portable source code, use conditional compilation so that the x86 edition does not read these keywords. For example, you can prevent the x86 edition of the compiler from seeing _ _unaligned by creating a macro, _ _unaligned, that is defined as an empty string when compiling for the x86 platform. See Working with Data Alignment Issues in Chapter 3 for an example.

The _ _int64 keyword is recognized by the x86 edition of Visual C++ but is not supported by the integrated debugger. Therefore, its use is not particularly recommended on the x86 platform.


_ _unaligned

The _ _unaligned keyword is a type modifier in pointer definitions, which indicates to the compiler that the data pointed to may not be properly aligned on a correct address. To be properly aligned, the address of an object must be a multiple of the size of the type. For example, 2-byte objects must be aligned on even addresses. When data is accessed through a pointer declared _ _unaligned, the compiler generates the additional code necessary to load or store, or read or write the data without causing alignment errors. It is best to avoid use of misaligned data altogether, but in some cases the usage may be justified by the need to access packed structures such as shared disk structures or I/O hardware.

Chapter 3, Porting Visual C++ Source Code, discusses the alignment problem and the use of _ _unaligned in detail.


_ _int64

The _ _int64 keyword declares a new type, a 64-bit (8-byte) integer. As with the int, short, and long types, the _ _int64 type has a corresponding unsigned version, so the _ _int64 keyword actually can be used to create two types.

The following code sample shows how to declare two 64-bit integers, one signed and the other unsigned:

__int64            signed_big_int;
unsigned __int64   unsigned_big_int;

In the printf family of run-time library functions, the format for optional prefixes includes I64, in addition to F, N, h, l, and L. For example, the following statement includes an example of a valid format string:

printf("%I64d", x);

When manipulating 64-bit integers, no special functions are necessary. Ordinary arithmetic operators and operations behave as expected.

Note Both the Alpha edition and the x86 edition of Visual C++ support the _ _int64 data type. However, the support in the Alpha edition is more complete; in particular, the integrated debugger recognizes _ _int64 variables in the Alpha edition but not in the x86 edition. Also, both editions provide full support for the _ _int8, _ _int16, and _ _int32 types. Use of these types is not recommended, except in situations where the program must interact with a fixed-byte layout (for example, in reading records previously stored on disk).

Use of _ _int64 should be conditional on the predefined macro _INTEGRAL_MAX_BITS. This macro describes the maximum size of integers defined using the form _ _int x. For example:

_#if defined (_INTEGRAL_MAX_BITS) && \
  _INTEGRAL_MAX_BITS >= 64
typedef signed __int64 int64;
typedef unsigned __int64 uint64;
#else
#error __int64 type not supported
#endif


Intrinsic Functions On Alpha - Overview

Intrinsic functions (also known as intrinsics) are function calls that the compiler resolves directly, by generating code, rather than by calling an external function. The Alpha version of the compiler supports a number of intrinsics.

With all intrinsics, you must explicitly enable the intrinsic before calling it, by using it with the /Oi compiler option or the intrinsic pragma. The general syntax for using this pragma is:

#pragma intrinsic( intrinsic-name )

For example, to enable the use of the _InterlockedIncrement intrinsic, include a pragma as shown, along with the declaration of the function:

long _InterlockedIncrement(long *);
#pragma intrinsic(_InterlockedIncrement)

The intrinsic functions listed in the following sections correspond one-for-one with existing instructions or PAL calls in the Alpha architecture. These functions assume that the following type definitions apply:

typedef signed _ _int 64  int 64;
typedef unsigned _ _int 64  uint64;

Please note that some intrinsics may only be executed in privileged context; for example, the _ _PAL_HALT intrinsic may only be successfully executed in kernel mode. For additional information, please consult the Alpha Architecture Reference Manual or Alpha Calling Standard (contained in Visual C++ Online Documentation).


Synchronization Intrinsics

This section describes several groups of intrinsic functions used to synchronize access to data in shared memory or to coordinate the execution of multiple concurrent portions of an application.


Atomic Memory Access Intrinsics with Infinite Retries

This group of intrinsic functions allows for interlocked access to longword and quadword memory data with the possibility of infinite retries in the presence of contention for access to the target memory. These functions have the following declarations:

int __ADD_ATOMIC_LONG2( void *, int );
int __AND_ATOMIC_LONG2( void *, int );
int __OR_ATOMIC_LONG2( void *, int );

__int64 __ADD_ATOMIC_QUAD2( void *, __int64 );
__int64 __AND_ATOMIC_QUAD2( void *, __int64 );
__int64 __OR_ATOMIC_QUAD2( void *, __int64 );

The first argument to these intrinsics is a pointer to the memory data item to be updated in an interlocked fashion. Note that this pointer must be naturally aligned for the type of access desired (longword or quadword).

The second argument to these intrinsics is an int or _ _int64 value, used in the interlocked ADD, AND, or OR operation.

The return value from these intrinsics is the contents of the memory location specified by the first argument before the ADD, AND, or OR operation is performed.

In the presence of contention with another thread to interlock the target memory location, these intrinsics will spinlock in a compute-intensive fashion in an attempt to acquire the memory interlock.

These intrinsics do not contain memory barrier instructions; developers must add them as required.


Atomic Memory Access Intrinsics with Limited Retries

This group of intrinsic functions allow for interlocked access to longword and quadword memory data with control over the number of retries attempted in the presence of contention for access to the target memory. These functions have the following declarations:

int __ADD_ATOMIC_LONG3( void *, int, int );
int __AND_ATOMIC_LONG3( void *, int, int );
int __OR_ATOMIC_LONG3( void *, int, int );

int __ADD_ATOMIC_QUAD3( void *, __int64, int );
int __AND_ATOMIC_QUAD3( void *, __int64, int );
int __OR_ATOMIC_QUAD3( void *, __int64, int );

The first argument to these intrinsics is a pointer to the memory data item to be updated in an interlocked fashion. Note that this pointer must be naturally aligned for the type of access desired (longword or quadword).

The second argument to these intrinsics is an int or _ _int64 value, used in the interlocked ADD, AND, or OR operation.

The third argument to these intrinsics specifies the maximum number of attempts used to acquire the interlock to the memory data item involved.

These intrinsics return 1 if the operation completed before the maximum number of retries was exceeded, 0 otherwise.

Note that these intrinsics do not contain memory barrier instructions; you are responsible for adding them as required.


Atomic Bit Test/Set Intrinsics with Infinite Retries

These two intrinsic functions allow for interlocked access to single bit memory data with the possibility of infinite retries in the presence of contention for access to the target memory. These functions have the following declarations:

int __TESTBITCCI2( void *, int );
int __TESTBITSSI2( void *, int );

The first argument to these intrinsics is a pointer to the base of an array of bits. The second argument is a zero-based bit number used to specify which bit in the array to set _ _TESTBITSSI2 or clear _ _TESTBITCCI2.

These intrinsics always return 1.

In the presence of contention with another thread to interlock the quadword memory location containing the bit of interest, these intrinsics will spinlock in a compute-intensive fashion in an attempt to acquire the memory interlock.

Note that these intrinsics do not contain memory barrier instructions; you are responsible for adding them as required.


Atomic Bit Test/Set Intrinsics with Limited Retries

These intrinsics have the following declarations:

int __TESTBITCCI3( void *, int, int );
int __TESTBITSSI3( void *, int, int );

These intrinsics function similarly to the two argument versions described in the previous section, with the addition of a third argument used to specify the maximum number of attempts to acquire the memory interlock. The intrinsics return 0 if the interlock was successfully acquired and the bit specified changed state as the result of the operation.

These intrinsics do not contain memory barrier instructions; you are responsible for adding them as required.


Semaphore Intrinsics

The semaphore intrinsics provide means for atomically incrementing, decrementing, and exchanging data with a “semaphore,” a protected data object used for communication with other processes on Windows NT. In each case, the data associated with the semaphore is treated as volatile by the compiler. During optimization, volatile objects are not assumed to be constant; other processes can change their values between readings of the value of the volatile object.

Each of these operations is interlocked. No other process can interfere with the operation of the function until it completes and exits.


_InterlockedIncrement

_InterlockedIncrement Intrinsic

The _InterlockedIncrement intrinsic has the following declaration:

long _InterlockedIncrement( long *lp );

This intrinsic function increments the value of the semaphore data location pointed to by lp and returns the new value.


_InterlockedDecrement

_InterlockedDecrement Intrinsic

The _InterlockedDecrement intrinsic has the following declaration:

long _InterlockedDecrement( long *lp );

This intrinsic function decrements the value of the semaphore data location pointed to by lp and returns the new value.


_InterlockedExchange

_InterlockedExchange Intrinsic

The _InterlockedExchange intrinsic has the following declaration:

long _InterlockedExchange( long *lp, long lnew );

This intrinsic function copies the value of lnew to the semaphore data location pointed to by lp. The function then returns the value of the semaphore before it was altered.


_InterlockedCompareExchange Intrinsic

The _InterlockedCompareExchange intrinsic has the following declaration:

void* _InterlockedCompareExchange(void **, void, void);

This intrinsic function performs an interlocked compare of the integer contents of memory specified by the longword-aligned first argument with the value of the third argument. If they are equal, then the value of the second argument is written into the memory location specified by the first argument. The intrinsic returns the previous value of the memory location specified by the first argument. This intrinsic contains required memory barriers.


_InterlockedExchangeAdd Intrinsic

The _InterlockedExchangeAdd intrinsic has the following declaration:

int _InterlockedExchangeAdd(int *, int);

This intrinsic function performs an interlocked add of the second argument to the longword-aligned integer specified by the first argument. The intrinsic returns the previous value of the memory location specified by the first argument. This intrinsic contains required memory barriers.


Spinlock Manipulation Intrinsics

This group of intrinsic functions allows for the creation and manipulation of spinlock synchronization objects.

These intrinsics should be used with caution as they consume processor resources while waiting for a spinlock to be released.


_AcquireSpinLock Intrinsic

The _AcquireSpinLock intrinsic has the following declaration:

void _AcquireSpinLock(long *);

This intrinsic function takes a single argument, a pointer to a longword-aligned memory spinlock. The intrinsic repeatedly attempts to acquire the spinlock in a compute-intensive manner, and with an infinite number of retries allowed. This intrinsic contains required memory barriers.


_AcquireSpinLockCount Intrinsic

The _AcquireSpinLockCount intrinsic has the following declaration:

int _AcquireSpinLockCount(long *, int);

This intrinsic function is used to acquire the longword-aligned spinlock pointed to by the first argument. Unlike _AcquireSpinLock, this intrinsic will limit the number of times it will try to acquire the spinlock; the value is specified by the second argument. The return value of this intrinsic is 1 if the spinlock was successfully acquired or 0 if the spinlock was not acquired before the retry count was exceeded. This intrinsic contains required memory barriers.


_ReleaseSpinLock Intrinsic

The _ReleaseSpinLock intrinsic has the following declaration:

void _ReleaseSpinLock(long *);

This intrinsic function releases the spinlock specified by the first argument. The spinlock must be longword-aligned. For more information, please see the descriptions of the _AcquireSpinLock and _AcquireSpinLockCount intrinsics in this chapter.


Miscellaneous Synchronization Intrinsics

This section details synchronization intrinsics that do not fall into any of the other categories.


_ _ADAWI Intrinsic

_ _ADAWI Intrinsic (VAX Compatibility Only)

The _ _ADAWI intrinsic has the following declaration:

int __ADAWI(short, short *);

This intrinsic function adds the value of its first argument to the word-aligned memory location specified by the second argument. The addition is accomplished while the target memory location is held under a memory interlock. The return value is a bitmask with the following interpretation:

  • Bit 0: If set, indicates a carry from the MSB of the result occurred.

  • Bit 1: If set, indicates the result overflowed.

  • Bit 2: If set, indicates that the result was zero.

  • Bit 3: If set, indicates that the result was less than zero.

This intrinsic is provided only for compatibility with existing VAX applications; it is suggested that new applications use other methods more appropriate to the Alpha architecture to achieve this capability.


_ _CMP_STORE_LONG Intrinsic

The _ _CMP_STORE_LONG intrinsic has the following declaration:

int __CMP_STORE_LONG(void *, int, int, void *);

This intrinsic function uses the first argument (a longword-aligned pointer) to compare the memory location specified with the contents of the second argument under a memory interlock. If the values differ, the intrinsic returns 0. If the values are the same, an attempt is made to store the value of the third argument into the longword-aligned memory location specified by the fourth argument using a store-conditional instruction. If the store is successful, the intrinsic returns 1; otherwise, it returns 0.

This intrinsic can be used to implement efficient guarded access to a data item (specified by the fourth argument) under control of a state variable (specified by the first argument).

This intrinsic does not contain memory barrier instructions; you are responsible for adding them as required.


_ _CMP_STORE_QUAD Intrinsic

The _ _CMP_STORE_QUAD intrinsic has the following declaration:

int __CMP_STORE_QUAD(void *, __int64, __int64, void *);

This intrinsic function is used to perform the same logical operation as was described in _ _CMP_STORE_LONG, but with _ _int64 data instead of int.

Note that this intrinsic does not contain memory barrier instructions; you are responsible for adding them as required.


Floating-Point Intrinsics

This group of intrinsic functions allows for access to specific floating-point hardware features normally only available to the assembly language programmer.

Using _ _asm, _ _dasm, and _ _fasm also allows access to specific floating-point hardware features.


_ _ADDS_C Intrinsic

The _ _ADDS_C intrinsic has the following declaration:

float __ADDS_C(float, float);

This intrinsic function returns the sum of the two IEEE single-precision floating-point arguments with IEEE chopped rounding used.


_ _ADDT_C Intrinsic

The _ _ADDT_C intrinsic has the following declaration:

double __ADDT_C(double, double);

This intrinsic function returns the sum of the two IEEE double-precision floating-point arguments with IEEE chopped rounding used.


_ _CPYS Intrinsic

The _ _CPYS intrinsic has the following declaration:

double __CPYS(double, double);

This intrinsic function returns the double-precision result of extracting the sign bit of the first argument and concatenating the exponent and fraction bits from the second argument. This has the effect that the result has the same sign as the first argument, but with the magnitude of the second argument.


_ _CPYSE Intrinsic

The _ _CPYSE intrinsic has the following declaration:

double __CPYSE(double, double);

This intrinsic function returns the double-precision result of extracting the sign and exponent of the first argument and concatenating the fraction bits from the second argument. This has the effect that the result has the same sign and exponent as the first argument, but with the fractional portion of the second argument.


_ _CPYSEF Intrinsic

The _ _CPYSEF intrinsic has the following declaration:

float __CPYSEF(float, float);

This intrinsic function returns the single-precision result of extracting the sign and exponent of the first argument and concatenating the fraction bits from the second argument. This has the effect that the result has the same sign and exponent as the first argument, but with the fractional portion of the second argument.


_ _CPYSF Intrinsic

The _ _CPYSF intrinsic has the following declaration:

float __CPYSF(float, float);

This intrinsic function returns the single-precision result of extracting the sign bit of the first argument and concatenating the exponent and fraction bits from the second argument. This has the effect that the result has the same sign as the first argument, but with the magnitude of the second argument.


_ _CPYSN Intrinsic

The _ _CPYSN intrinsic has the following declaration:

double __CPYSN(double, double);

This intrinsic function performs a similar action as the _ _CPYS intrinsic, but where the sign of the first argument is negated before being combined with the magnitude of the second argument. This has the effect that the result has the opposite sign as the first argument, but has the magnitude of the second argument.


_ _CPYSNF Intrinsic

The _ _CPYSNF intrinsic has the following declaration:

float __CPYSNF(float, float);

This intrinsic function performs a similar action as the _ _CPYSF intrinsic (described above), but where the sign of the first argument is negated before being combined with the magnitude of the second argument. This has the effect that the result has the opposite sign as the first argument, but has the magnitude of the second argument.


_ _CVTQS_C Intrinsic

The _ _CVTQS_C intrinsic has the following declaration:

float __CVTQS_C(__int64);

This intrinsic function returns the IEEE single-precision result of converting an _ _int64 value to IEEE single-precision floating-point format, with IEEE chopped rounding used.


_ _CVTQT_C Intrinsic

The _ _CVTQT_C intrinsic has the following declaration:

double __CVTQT_C(__int64);

This intrinsic function returns the IEEE double-precision result of converting an _ _int64 value to IEEE double-precision floating-point format, with IEEE chopped rounding used.


_ _CVTTQ Intrinsic

The _ _CVTTQ intrinsic has the following declaration:

__int64 __CVTTQ(double);

This intrinsic function returns the result of converting an IEEE double-precision value to an _ _int64 value.

This intrinsic can overflow, with errors handled according to the particular /QAieee switch specified. For more information, see the Floating-Point Exception-Handling Options topic in this chapter.


_ _CVTTS_C Intrinsic

The _ _CVTTS_C intrinsic has the following declaration:

float __CVTTS_C(double);

This intrinsic function returns the IEEE single-precision result of converting an IEEE double-precision value to IEEE single-precision floating-point format, with IEEE chopped rounding used.

This intrinsic can both underflow and overflow, with errors handled according to the particular /QAieee switch specified. For more information, see the Floating-Point Exception-Handling Options topic in this chapter.


_ _DIVS_C Intrinsic

The _ _DIVS_C intrinsic has the following declaration:

float __DIVS_C(float, float);

This intrinsic function returns the result of dividing the first argument by the second argument, with IEEE chopped rounding used. Both arguments and the result are in IEEE single-precision format.


_ _DIVT_C Intrinsic

The _ _DIVT_C intrinsic has the following declaration:

double __DIVT_C(double, double);

This intrinsic function returns the result of dividing the first argument by the second argument, with IEEE chopped rounding used. Both arguments and the result are in IEEE double-precision format.


_ _MULS_C Intrinsic

The _ _MULS_C intrinsic has the following declaration:

float __MULS_C(float, float);

This intrinsic function returns the result of multiplying the first argument with the second argument, with IEEE chopped rounding used. Both arguments and the result are in IEEE single-precision format.


_ _MULT_C Intrinsic

The _ _MULT_C intrinsic has the following declaration:

double __MULT_C(double, double);

This intrinsic function returns the result of multiplying the first argument with the second argument, with IEEE chopped rounding used. Both arguments and the result are in IEEE double-precision format.


_ _SUBS_C Intrinsic

The _ _SUBS_C intrinsic has the following declaration:

float __SUBS_C(float, float);

This intrinsic function returns the result of subtracting the second argument from the first argument, with IEEE chopped rounding used. Both arguments and the result are in IEEE single-precision format.


_ _SUBT_C Intrinsic

The _ _SUBT_C intrinsic has the following declaration:

double __SUBT_C(double, double);

This intrinsic function returns the result of subtracting the second argument from the first argument, with IEEE chopped rounding used. Both arguments and the result are in IEEE double-precision format.


Program Control Intrinsics

This set of varied intrinsics allow for access to the privileged state of the Alpha processor via PAL code instructions.

These intrinsics are often used when writing several mode code, such as device drivers.


_ _ei Intrinsic

The _ _ei intrinsic has the following declaration:

void __ei(void);

This intrinsic function executes a CALL_PAL EI (Enable Interrupts) instruction. This intrinsic may only be executed in kernel mode.


_ _di Intrinsic

The _ _di intrinsic has the following declaration:

void __di(void);

This intrinsic function executes a CALL_PAL DI (Disable Interrupts) instruction. This intrinsic may only be executed in kernel mode.


_ _PAL_HALT Intrinsic

The _ _PAL_HALT intrinsic has the following declaration:

void __PAL_HALT(void);

This intrinsic function executes a CALL_PAL HALT instruction. This intrinsic may only be executed in kernel mode.


_ _PAL_IMB Intrinsic

The _ _PAL_IMB intrinsic has the following declaration:

void __PAL_IMB(void);

This intrinsic function executes a CALL_PAL IMB (Instruction Memory Barrier) instruction, used to make the processor instruction stream coherent with system memory.

Use this instruction when writing self-modifying code. You must insert this instruction prior to executing code that has been modified; otherwise, the processor behavior is unpredictable.


_ _rdpcr Intrinsic

The _ _rdpcr intrinsic has the following declaration:

void *__rdpcr(void);

This intrinsic function executes a CALL_PAL RDPCR (Read Processor Control Region) instruction. The return value is the base address of the Processor Control Region (PCR) for the current processor. This intrinsic may only be executed in kernel mode.


_rdteb Intrinsic

The _rdteb intrinsic has the following declaration:

void *_rdteb(void);

This intrinsic function executes a CALL_PAL RDTEB (Read Thread Environment Block) instruction. The return value is the base address of the current Thread Environment Block (TEB) for the currently executing thread on the current processor.


_ _rdthread Intrinsic

The _ _rdthread intrinsic has the following declaration:

void *__rdthread(void);

This intrinsic function executes a CALL_PAL RDTHREAD (Read Current Thread) instruction. The return value is the thread object address for the currently executing thread on the current processor.

This intrinsic may only be executed in kernel mode.


_ _ssir Intrinsic

The _ _ssir intrinsic has the following declaration:

void __ssir(unsigned long);

This intrinsic function executes a CALL_PAL SSIR (Request Software Interrupt) instruction. The argument specified is a bit mask of interrupts to be requested, with the following interpretation:

  • Bit 0: Request dispatch software interrupt

  • Bit 1: Request APC software interrupt

This intrinsic may only be executed in kernel mode.


_ _swpirql Intrinsic

The _ _swpirql intrinsic has the following declaration:

unsigned char __swpirql(unsigned char);

This intrinsic function executes a CALL_PAL SWPIRQL (Swap Interrupt Request Level) instruction. The argument to the intrinsic is used to supply the new IRQL value; the intrinsic returns the previous IRQL.

This intrinsic may only be executed in kernel mode.


_ _tbia Intrinsic

The _ _tbia intrinsic has the following declaration:

void __tbia(void);

This intrinsic function executes a CALL_PAL TBIA (Translation Buffer Invalidate All) instruction. This will invalidate all cached virtual address translations for the current processor that are not fixed. Some translations may be fixed in hardware or software and these are not invalidated.

This intrinsic may only be executed in kernel mode.


_ _tbis Intrinsic

The _ _tbis intrinsic has the following declaration:

void __tbis(void *);

This intrinsic function executes a CALL_PAL TBIS (Translation Buffer Invalidate Single) instruction. The single argument is the virtual address to be invalidated, using the current address space number (ASN). This will invalidate any cached virtual address translations for that address. Note that it is possible on some implementations that additional addresses may also be invalidated.

This intrinsic may only be executed in kernel mode.


_ _tbisasn Intrinsic

The _ _tbisasn intrinsic has the following declaration:

void __tbisasn(void *, unsigned long);

This intrinsic function executes a CALL_PAL TBISASN (Translation Buffer Invalidate Single Address Space Number) instruction. The first argument is the virtual address to be invalidated. The second argument is used to specify the address space number to be used. This will invalidate any cached virtual address translations for that address for the specified address space number. As with _ _tbis, some implementations may invalidate more translations than the single one specified.

This intrinsic may only be executed in kernel mode.


_ _dtbis Intrinsic

The _ _dtbis intrinsic has the following declaration:

void __dtbis(void *);

This intrinsic function executes a CALL_PAL DTBIS (Data Translation Buffer Invalidate Single) instruction. The single argument is the virtual address to be invalidated. This will invalidate any data stream cached virtual address translations for that address. Note that it is possible on some implementations that additional addresses may also be invalidated.

This intrinsic can only be executed in kernel mode.


Miscellaneous Intrinsics

This group of intrinsics does not fall into any of the other categories.


_ _BPT Intrinsic

The _ _BPT intrinsic inserts a breakpoint instruction.

void __BPT(void);
#pragma intrinsic(__BPT)


_ _UMULH Intrinsic (Unsigned Quadword Multiply High)

The _ _UMULH intrinsic has the following declaration:

uint64 __UMULH( uint64 operand1, uint64 operand2 );

This intrinsic function multiplies the two operands as unsigned numbers to produce a 128-bit result. The high-order 64 bits of the result are returned.


_ _TRAPB Intrinsic (Trap Barrier)

_ _Trap Barrier Intrinsic (TRAPB)

The _ _TRAPB intrinsic has the following declaration:

void __TRAPB( void );

This intrinsic function guarantees that the following code does not execute until all possible preceding traps have been signaled.


_ _RCC Intrinsic (Read Cycle Counter )

The _ _RCC intrinsic has the following declaration:

unsigned int __RCC( void );

This intrinsic function obtains the contents of the process cycle counter, which is useful for timing small segments of performance-sensitive code.


_ _GENTRAP Intrinsic (Generate Trap)

The _ _GENTRAP intrinsic has the following declaration:

void __PAL_GENTRAP( uint64 trap_code );

This intrinsic function reports run-time software conditions. The processor is switched to kernel mode and an exception is dispatched.

Math RTLs may use this mechanism. For example, integer divide by zero is signaled using GENTRAP.


_ _MB Intrinsic (Memory Barrier)

The _ _MB intrinsic has the following declaration:

void __MB( void );

This intrinsic function guarantees that all subsequent loads and stores of data do not access memory until after all previous loads and stores have been completed, as observed by other processors in a multiprocessor system. You do not need to use this function in single-processor systems, except when performing physical I/O.

Memory barriers are very important when writing device drivers, kernel mode code, and multithreaded applications. Note, however, that if a multithreaded application uses the system semaphore application programming interface (API) functions, it need not be concerned with memory barriers because the API functions already have memory barriers included.


_ _setjmp Intrinsic

The _ _setjmp intrinsic has the following declaration:

int __setjmp( jmp_buf buffer );

When used in conjunction with longjmp, this intrinsic function provides a way to execute a nonlocal goto. To obtain the correct intrinsic definition of _ _setjmp for Alpha, you must use #include <setjmp.h> or <setjmpex.h>. Correct interaction with C structured exception handling and C++ exception-handling extensions requires the use of #include <setjmpex.h>.


Support for Microsoft C Intrinsics

The Alpha edition of Visual C++ supports the following Microsoft C intrinsic functions:

_ _alloca | _ _lrotl | memcmp | strcpy
abs | _ _lrotr | memcpy | strlen
fabs | _ _rotl | memset |
labs | _ _rotr | strcmp |

Note that the _ _inp and _ _outp intrinsics are specific to x86 and are not supported by standard library functions.

For the remaining Microsoft C intrinsic functions, the Alpha compiler generates normal external calls to the standard library functions regardless of whether the intrinsic forms are enabled by command-line options or pragmas.


Windows NT for Alpha Assembler

Included with Microsoft Visual C++ for Alpha is an assembler (ASAXP.EXE) that converts assembly language statements into Alpha machine code. For information on using the Windows NT for Alpha Assembler, see the Help icon in the Visual C++ program group.


IEEE Floating-Point Behavior on Alpha

This section describes how the Alpha architecture implements IEEE floating-point behavior and how to use the available rounding modes.


Default Floating-Point Optimizations on Alpha

Disable Default Optimizations

When developing programs where a high degree of floating-point accuracy is important, use the /Op switch to disable arithmetic identity optimizations. Along with other non-floating-point optimizations that are disabled with this switch, this switch also disables floating-point optimizations; for example, using inverse multiplication instead of division, which can reduce floating-point accuracy.


Round to Nearest

Compile your program with /QArn or call _ _controlfp(_RC_NEAR, _MCW_RC) after compiling your program with /QArd, dynamic rounding mode. Using the /QArn switch results in a faster program because the call to _ _controlfp is avoided.


Round to Zero (Chop)

Compile your program with /QArc or call _ _controlfp(_RC_CHOP, _MCW_RC) after compiling your program with /QArd. Using the /QArc switch results in a faster program because the call to _ _controlfp is avoided.


Round to Minus Infinity

Compile your program with /QArm or call _ _controlfp(_RC_DOWN, _MCW_RC) after compiling your program with /QArd. Using the /QArm switch results in a faster program because the call to _ _controlfp is avoided.


Round to Plus Infinity

Compile your program with /QArd and call _ _controlfp(_RC_UP, _MCW_RC).

Using the /QArd switch on Alpha systems results in slower performance because the rounding is calculated in software.


Nonfinite Values

This section discusses how the Alpha architecture handles denormal, plus and minus infinity, and NaN values, and provides suggestions for maximizing floating-point performance with these types of values.


Denormal Values

Systems based on the Alpha processor do not support denormal values in hardware. The use of denormal values slows these systems because the denormal values must be calculated in software.

To get maximum floating-point performance for applications that do not deal with these extremely small numbers, these denormal values are rounded to zero by default when they first occur. This behavior is the default of both the compiler and Windows NT.

For applications that require denormal values, the default compiler behavior must be changed by using the /QAieee switch, and the Windows NT default behavior must be changed by calling _ _controlfp(DN_SAVE_MCW_DN).

Prior to Windows NT 5.0, only denormal results were rounded to zero. Starting with Windows NT 5.0, both denormal input operands and denormal results are rounded to zero. Both denormal input operands and denormal results are preserved by calling _ _controlfp(_DN_SAVE, _MCW_DN). You can restore the pre-Windows NT 5.0 behavior by calling _ _controlfp(_DN_SAVE_OPERANDS_FLUSH_RESULTS, _mcw_dn). The opposite behavior can be created by calling _ _controlfp(_DN_FLUSH_OPERANDS_SAVE_RESULTS, _MCW_DN).

Neither saving input operands and flushing results nor saving results and flushing input operands are used in practice. They are only provided for completeness.


Infinity (+ and -) and NaN Values

The 21064 Alpha processor does not support infinity and NaN values in hardware. The use of infinity and NaN values slows these systems based on this processor because these values must be calculated in software.

To get maximum floating-point performance for applications that do not deal with infinity and NaN values, these values cause an exception by default, to provide an opportunity to correct what is normally an error condition. This behavior is the default of the compiler.

For applications that require infinity and NaN values without exceptions, the default compiler must be changed by using the /QAieee switch, and the Windows NT default behavior must be changed by calling _ _controlfp(_EM_INEXACT, _MCW_EM), which will disable all exceptions except for inexact results. Inexact exceptions are not generated unless the /QAieee2 compiler option is used.


Floating-Point Exception Modes

Systems based on the DIGITAL Alpha processor do not support nonfinite floating-point values (denormals, infinity, and NaN) in hardware. The Alpha processor has two ways of delivering exceptions raised by these values:

  • Imprecise, which speeds up normal floating-point operations

  • Precise, which identifies the exact instruction that failed

The default exception behavior of the compiler is as follows:

  • Underflow exceptions disabled, and underflow values converted to true zero (not minus zero).

  • Inexact result exception disabled.

  • Invalid operation imprecise exception enabled.

  • Division-by-zero exception enabled.

  • Floating-point overflow exception enabled.

This default results in faster operation for C and C++ applications and provides correct floating-point exception handling for both C structured exception handling and C++ exception handling.

This default is not adequate, however, for applications that need to identify the precise machine instruction that caused the exception, and/or use nonfinite values (see previous section). Both of these types of applications must use precise exceptions, which, because of the overhead involved, causes all floating-point operations to slow down. The /QApe compiler option enables precise exceptions for the exceptions specified above.


Underflow Exceptions

For applications that require underflow exceptions, the default compiler behavior must be changed by using the /QAieee switch. The Windows NT behavior can then be changed to enable underflows to denormal values and to enable their associated exceptions by calling _ _controlfp(_DN_SAVE | 0, _MCW_DN | EM_UNDERFLOW). By default Windows NT silently rounds denormal values to zero. Underflow exceptions can subsequently be disabled by calling _controlfp(EM_UNDERFLOW, EM_UNDERFLOW). In both cases, calling _ _controlfp creates additional overhead.

The underflow status bit is set when a floating-point operation underflows. The status bit is set regardless of whether the underflow exception is masked or unmasked. Having the status bit set does not result in underflow exceptions on subsequent floating-point instructions. For compatibility with other types of processors, however, you should clear the bit with _ _clearfp() when underflow exceptions are unmasked.


Invalid Operation Exceptions

For applications that require invalid operation exceptions, the default compiler behavior must be changed by using the /QAieee switch. The Windows NT behavior can then be changed to enable invalid operation exceptions by calling _ _controlfp(O, _EM_INVALID). By default, invalid operations generate a value that is Not a Number (NaN) and no exception is raised. Invalid operation exceptions can subsequently be disabled by calling _ _controlfp(EM_INVALID, EM_INVALID). In both cases, calling _ _controlfp creates additional overhead.

The underflow status bit is set when a floating-point operation underflows. The status bit is set regardless of whether the underflow exception is masked or unmasked. Having the status bit set does not result in underflow exceptions on subsequent floating-point instructions. For compatibility with other types of processors, however, you should clear the bit with _ _clearfp() when underflow exceptions are unmasked.


Division-by-Zero Exceptions

For applications that require division-by-zero floating-point exceptions, either the default imprecise exceptions can be used or the default compiler behavior can be changed by using the /QAieee switch. If the /QAieee switch is used, then the Windows NT behavior must be changed to enable divide-by-zero exceptions by calling _ _controlfp(0, EM_ZERODIVIDE). If the /QAieee switch is used, a divide-by-zero operation generates an infinite value and no exception is raised. Divide-by-zero exceptions can subsequently be disabled by calling _ _controlfp(EM_ZERODIVIDE, EM_ZERODIVIDE). For both calls _ _controlfp creates additional overhead.

The underflow status bit is set when a floating-point operation underflows. The status bit is set regardless of whether the underflow exception is masked or unmasked. Having the status bit set does not result in underflow exceptions on subsequent floating-point instructions. For compatibility with other types of processors, however, you should clear the bit with _ _clearfp() when underflow exceptions are unmasked.


Overflow Exceptions

For applications that require overflow floating-point exceptions, imprecise exceptions (default) can be used, or the default compiler behavior can be changed by using the /QAieee switch. If the /QAieee switch is used, the Windows NT behavior must be changed to enable divide-by-zero exceptions by calling _ _controlfp(0, EM_OVERFLOW).

The default behavior when the /QAieee switch is used is that an overflow generates an infinite value and no exception is raised. Divide-by-zero exceptions can subsequently be disabled by calling _ _controlfp(EM_OVERFLOW, EM_OVERFLOW). For both calls, _ _controlfp creates additional overhead.

The underflow status bit is set when a floating-point operation underflows. The status bit is set regardless of whether the underflow exception is masked or unmasked. Having the status bit set does not result in underflow exceptions on subsequent floating-point instructions. For compatibility with other types of processors, however, you should clear the bit with _ _clearfp() when underflow exceptions are unmasked.


Inexact Result Exceptions

For applications that require inexact result exceptions, the default compiler behavior must be changed by using the /QAieee2 switch. The Windows NT behavior can then be changed to enable inexact result exceptions by calling _ _controlfp(O, _EM_INEXACT). Underflow exceptions can then be disabled by calling _ _controlfp(_EM_INEXACT, _EM_INEXACT). Both cases cause the worst floating-point performance because even when exceptions are disabled, each inexact calculation requires software support, and most floating-point operations in a typical operation are inexact.

The underflow status bit is set when a floating-point operation underflows. The status bit is set regardless of whether the underflow exception is masked or unmasked. Having the status bit set does not result in underflow exceptions on subsequent floating-point instructions. For compatibility with other types of processors, however, you should clear the bit with _ _clearfp() when underflow exceptions are unmasked.


Chapter 6 Using the Alpha Inline Assembler

A limited inline assembler is built into the Alpha version of the Visual C++ compiler. Inline assembly code can be a powerful tool for those tasks that are otherwise very difficult or impossible to accomplish in C/C++. Additionally, using the inline assembler may be more convenient than using a separate assembler because it doesn’t require an additional assembly step.

A key feature of the inline assembler is that it allows you to pass references to C/C++ objects and expressions into the assembly code you write. This feature makes it possible to write useful C/C++ macros consisting of assembly code, and to apply these macros to different objects and expressions with C/C++ code.

This chapter covers the following topics:

  • Using Inline Assembly with Visual C++ for Alpha

  • Learning the Alpha Assembly Language

  • Alpha Inline Assembler Syntax

  • Referring to Integer Registers in Alpha Inline Assembler Code

  • Referring to Floating-Point Registers in Alpha Inline Assembler Code

  • Alpha Assembly Language Examples


Using Inline Assembly with Visual C++ for Alpha

In each C++ compilation unit that uses assembly statements, you are responsible for including function prototypes to declare each assembly statement that you will be using in that module. In addition, you must include a #pragma intrinsic statement declaring each assembly statement type you wish to use. For example, the following C++ code can be used to accomplish this definition:

extern “C” {
   long __asm(char *, ...);
   double __dasm(char *, ...);
   float __fasm(char *, ...);
   };
#pragma intrinsic (__asm, __dasm, __fasm)

Note It is not necessary to perform these declarations in C source modules.

As can be inferred from the declaration above, assembly statements have function call semantics:

  • The first argument (argument 0) is the body of the function and is a literal string containing the inline assembler code. See the next section for a description of the syntax within this string.

  • Subsequent arguments are processed and evaluated exactly as C function call arguments. The first six arguments (arguments 1–6) are placed in the argument registers $16..$21 and/or $f16..$f21, depending on the type of the argument (for struct types, see the Alpha Calling Standard). Any remaining arguments are collected in a memory argument list that is a naturally aligned array of quadwords. This list is passed at 0($sp). For additional information on the details of argument-to-register correspondence, please see Section 4.1.2, “Normal Argument List Structure,” of the Alpha Calling Standard.

  • The result is taken from the return register corresponding to the type of the assembly statement (see the following explanation).

Assembly statements are specified using the function names _ _asm, _ _fasm, and _ _dasm. Code supplied via the _ _asm function name is assumed to return an integral result in $0; the other forms of assembler statements return single (_ _fasm) and double-precision (_ _dasm) floating-point results in $f0. You are responsible for arranging your assembly code so as to place the result in the appropriate register. As with any C/C++ function, it is possible to use an assembler statement without using the result, in which case the contents of the result register are ignored.

The assembly statement “body” (first argument) must contain valid assembler input. There may be assembler strings that the compiler fails to fully analyze. When this occurs, the compiler will generate a .S file directly from the assembler “body” and call the assembler to process them. Conservative optimization assumptions will be made when a .ASM file is necessary.

Because the Alpha compiler (unlike the x86 compiler) allows full optimization to occur in the presence of inline assembly code and performs minimization of instruction overhead when entering and leaving the inline assembly code, the following assembly code practices are illegal:

  • Branches in or out of inline assembly code to the surrounding C/C++ source code.

  • Communication of registers across separate instances of inline assembler code.

  • Connecting separate instances of inline assembler code through branches and labels.

Use caution when using direct references to the stack pointer for anything other than referring to arguments.


Learning the Alpha Assembly Language

The Alpha architecture and instruction set differ substantially from the x86 processor family. Please read your Alpha documentation thoroughly before writing Alpha assembly code. In addition, please note that the basic syntax of assembler statements on Alpha differs from other platforms. Specifically, note that the use of braces ({}) to delimit assembly code, as allowed on x86 platforms, is not supported on Alpha and will cause syntax errors at compile time.

One resource for learning Alpha assembly language is the online help file included with ASAXP — the Alpha Assembler for Windows NT. The help file, ASXAP.CHM, is included on the MSDN CD included with your copy of Visual C++, and is listed in the Visual C++ documentation Table of Contents.


Alpha Inline Assembly Syntax

As was previously described, __asm, __dasm, and __fasm are treated as extensions to the C/C++ language. Each could be viewed as having a prototype declaration as follows, with the string argument (char *) receiving special processing by the compiler:

__int64 __asm(char *, ...);
double __dasm(char *, ...);
float __fasm(char *, ...);

In this syntax, the string argument is a null-terminated character string consisting of one or more ASAXP assembly language statements with registers specified using a special metalanguage. The metalanguage is compatible with that allowed by DEC C for DIGITAL UNIX. Subsequent arguments are processed as if they were arguments to any C/C++ function call, but are passed as arguments to the assembly code described by the first string argument. For example, consider the following code:

c = __dasm(“mult %0, %1, $f0”, 4., 5.);

In this example, the reference to %0 refers to the first argument in the generated assembly code (with a value of 4). In a similar fashion, %1 refers to the second argument to the generated assembly code (with a value of 5).

Within the string argument, an embedded semicolon (;) or newline character (\n) is used to separate multiple assembly language statements. For example:

c = __dasm(“mult %0, %1, $f0;”
      “mult $f0, $f0”, 4., 5.);

Note that a semicolon is still required to separate statements, despite the fact that the strings are on separate physical lines in the source code.

The next example uses one string continued over two lines. The explicit statement separator is still required, but in this case, a newline character (\n) is used instead of a semicolon.

c = __dasm(“mult %0, %1, $f0\n\
    mult $f0, $f0”, 4., 5.);

Note that the compiler interprets the line-continuation character (\) by simply reading characters on the next line. No newline characters are read into the string at compile time, unless explicit newline characters (\n) are embedded in the string.

References to registers can be made by using conventions similar to those used by the Alpha ASAXP stand-alone assembler. Additionally, for compatibility across the Alpha operating systems, the acc metalanguage for register aliases is allowed. Register names must be specified in lowercase.


Referring to Integer Registers in Alpha Inline Assembly Code

Alpha systems have 32 integer registers, each of which is 64 bits wide. Integer registers are sometimes referred to as general registers in other system architectures. Integer registers have the names r0 to r31. Integer register r31 always contains the value 0. All other integer registers can be used interchangeably, except for integer register r30, which is assumed to be the stack pointer by certain PAL code.

Note that, for historical reasons, it is possible to use several alias names for the integer registers on Alpha and the compiler will accept any of the forms listed in the following table. When creating the optional machine code listing (.COD) file, the compiler will display all integer register references using the following preferred names: v0, t0..t12, s0..s5, a0..a5, ra, at, gp, sp, or zero.

The following table describes the Alpha integer register naming conventions for Windows NT. For additional information, please see the Windows NT for Alpha Systems Calling Standard, Revision 1.11, which is included in Books Online with Visual C++ for Alpha.

Register name | Alternate names (for compatibility across DIGITAL operating systems) | Usage
r0 | v0, $0, $v0, %v0, $r0, or %r0 | Function value register. In a standard call that returns a non-floating-point function result in a register, the result must be returned in this register. In a standard call, this register may be modified by the called procedure without being saved and restored.
r1..r8 | t0..t7, $1..$8, $t0..$t7, %t0..%t7, $r1..$r8, or %r1..%r8 | Conventional scratch registers. In a standard call, these registers may be modified by the called procedure without being saved and restored.
r9..r14 | s0..s5, $9..$14, $s0..$s5, %s0..%s5, $r9..$r14, or %r9..%r14 | Conventional saved registers. If a standard-conforming procedure modifies one of these registers, it must save and restore it.
r15 | s6, fp, $15, $s6, $fp, %s6, %fp, $r15, or %r15 | Stack Frame Base register. For procedures with a run-time variable amount of stack, this register is used to point at the base of the stack frame (fixed part of the stack). For all other procedures, this register has no special significance. If a standard-conforming procedure modifies this register, it must save and restore it.
r16..r21 | a0..a5, $16..$21, $a0..$a5, %a0..%a5, %0..%5, $r16..$r21, or %r16..%r21 | Argument registers. In a standard call, up to six non-floating-point items of the argument list are passed in these registers. In a standard call, these registers may be modified by the called procedure without being saved and restored.
r22..r25 | t8..t11, $22..$25, $t8..$t11, %t8..%t11, $r22..$r25, or %r22..%r25 | Conventional scratch registers. In a standard call, this register may be modified by the called procedure without being saved and restored.
r26 | ra, $ra, %ra, $26, $r26, or %r26 | Return Address register. In a standard call, the return address must be passed and returned in this register.
r27 | t12, $t12, %t12, $27, $r27, or %r27 | Conventional scratch register. In a standard call, this register may be modified by the called procedure without being saved and restored.
r28 | at, $at, %at, $28, $r28, or %r28 | Volatile scratch register. The contents of this register are always unpredictable after any external transfer of control either to or from a procedure. This applies to both standard and nonstandard calls. This register may be used by the operating system for external call fixup, autoloading, and exit sequences.
r29 | gp, $gp, %gp, $29, $r29, or %r29 | Global pointer or saved register. In a main (static) image, this register contains a pointer to a region of global storage (addresses and variable) constructed by the linker. In a dynamic-link library (DLL), this register must not be used in any way.
r30 | sp, $sp, %sp, $30, $r30, or %r30 | Stack Pointer. This register contains a pointer to the top of the current operating stack. Aspects of its usage and alignment are defined by the hardware architecture. Various software aspects of its usage and alignment are defined in the Windows NT for Alpha Systems Calling Standard, Section 3.2.1, Call Conventions.
r31 | zero, $31, $zero, %zero, $r31, %r31 | ReadAsZero/Sink. Hardware defined: binary zero as a source operand, sink (no effect) as a result operand.

Note Because Windows NT does not have a notion of pv (procedure value), the $pv and %pv forms of register specifiers used on DEC™ C for DIGITAL UNIX are not available on Windows NT.


Referring to Floating-Point Registers in Alpha Inline Assembly Code

Alpha systems have 32 floating-point registers, each of which is 64 bits wide. Each register can hold one single-precision (32-bit) value or one double-precision (64-bit) value. The floating-point registers have the names f0 to f31. Floating-point register f31 always contains the value 0. All other floating-point registers can be used interchangeably.

Note that, for historical reasons, it is possible to use several alias names for the floating-point registers on Alpha. The compiler will accept any of the forms listed in the following table. When creating the optional machine code listing (.COD) file, the compiler will display all floating-point register references using the following preferred names: v0, t0..t12, s0..s5, a0..a5, ra, at, gp, sp, or zero.

The following table describes the Alpha floating-point register naming conventions for Windows NT. For additional information, please see the Windows NT for Alpha Systems Calling Standard, Revision 1.11, which is included in Books Online with Visual C++ for Alpha.

Register name | Alternate names (for compatibility across DIGITAL operating systems) | Usage
f0 | $f0, %f0 | Floating-point function value register. In a standard call that returns a floating-point result in a register, this register is used to return the real part of the result. In a standard call, this register may be modified by the called procedure without being saved and restored.
f1 | $f1, %f1 | Floating-point function value register. In a standard call that returns a complex floating-point result in registers, this register is used to return the imaginary part of the result. In a standard call, this register may be modified by the called procedure without being saved and restored.
f2..f9 | $f2..f9, %f2..%f9 | Conventional saved registers. If a standard-conforming procedure modifies one of these registers, it must save and restore it.
f10..f15 | $f10..$f15, %f10..%f15 | Conventional scratch registers. In a standard call, it may be modified by the called procedure without being saved and restored.
f16..f21 | $f16..$f21, %f16..%f21, or %0..%5 | Argument registers. In a standard call, up to six floating-point arguments may be passed by value in these registers. In a standard call, these registers may be modified by the called procedure without being saved and restored.
f22..f30 | $f22..$f30, %f22..%f30 | Conventional scratch registers. In a standard call, these registers may be modified by the called procedure without being saved and restored.
f31 | $f31, %f31 | ReadAsZero/Sink. Hardware defined: binary zero as a source operand, sink (no effect) as a result operand.


Alpha Assembly Language Examples

With the Alpha edition of the Visual C++ compiler, the _ _asm, _ _fasm, and _ _dasm statements, introduced in the previous chapter, are especially flexible and powerful. You can use these statements, collectively referred to as “_ _asm statements,” within C/C++ source code in many different ways. This section provides two examples that illustrate some of the fine points of these _ _asm statements, illustrate how instructions are generated, and show how inline assembly code interacts with surrounding C/C++ source code.

The Microsoft Visual C++ compiler generates high-quality Alpha assembly-language code. Therefore, use _ _asm statements only when necessary; for example, when the Visual C++ compiler is not optimizing a certain section of code to your expectations. The examples shown here have been designed to illustrate the use of _ _asm statements; they are not inherently any more efficient than the code typically produced directly by the Visual C++ compiler.

This section has the following topics:

  • Example 1: Alpha Multiplication Function with Overflow

  • Example 2: Alpha Macro Using Inline Assembly

These examples are designed to show the very natural way in which _ _asm statements are often incorporated into C/C++ code on Alpha, via parameterized macros.


Example 1: Alpha Multiplication Function with Overflow

With the Alpha edition of the compiler, the _ _asm statement can be used to write macros that can be called from within C/C++ source code. The syntax of the _ _asm statement makes such macros possible because of the flexible support for arguments. As illustrated in the following example, the arguments do not have to be fixed, and they can even be specified at the time of macro expansion. However, this macro must be called only from within C/C++ functions.

In this example, the sample macro implements a function named my_mult, which takes two unsigned 32-bit integer arguments, a and b, and multiplies them together. The product is stored at the location specified by the third argument, c, which is a pointer to a 32-bit argument. The function also determines what the overflow portion of the multiplication is, if any, and returns the overflow portion via its return value.

// C++ only, declare asm statement as intrinsic: //

#ifdef __cplusplus
   extern "C" { __int64 __asm(char *,...); };
#pragma intrinsic(asm)
#endif

// long my_mult(long a, long b, long* c) //
// Action: *c <- a*b, return overflow   //

#define my_mult(a,b,c)
   asm("zapnot %0,15,%0;"      // zero-extend a
      "zapnot  %1,15,%1;"      // zero-extend b
      "mulq    %0,%1,t0;"      // multiply 64 bits
      "stl     t0,0(%2);"      // save low 32 bits
      "srl     t0,32,v0",      // and high 32 bits
      a,b,c)                   // pass arguments

The following is a simple driver program that invokes this macro:

void main()
{
   long m1 = 0xf0000000,       // multiplicands
      m2 = 0x00001010,
      product,overflow;        // results

   overflow = my_mult(m1,m2,&product);
   printf("%08X x %08X = %08X%08X\n\n",
      m1,m2,overflow,product);
}

Note the ways in which the C/C++ code and the assembly code interact in the preceding example:

  • Because conditional compilation is not used in this case, the code is not portable to other platforms. To make it portable, #ifdef blocks would have to be used.

  • The _ _asm statement inside the macro refers to its arguments, a, b, and c, using the register specifiers %0, %1, and %2, respectively. These are the first three argument registers; they can also be referred to as %a0, %a1, and %a2.

    The Alpha Visual C++ compiler will generate the appropriate sequence of instructions to evaluate the arguments supplied to an _ _asm statement and load the results in the appropriate machine registers before instantiating the user-supplied _ _asm statement code.

In the preceding example, all of the individual instructions in the body of the _ _asm statement are encoded in a single _ _asm statement, with all of the arguments to the _ _asm statement supplied in one place. Note that it is possible to code this example using individual _ _asm statements; this choice is left up to you.

In either case, the compiler generates the code shown in the following example for the invocation of my_mult inside of main. Notice that most of the _ _asm statements are assembled as instructions in a one-to-one relationship between those in the body of the _ _asm statement and the instructions listed. However, there are several exceptions to this general one-to-one relationship:

The compiler may reorder the sequence of generated instructions so as to optimize the overall flow of data through the processor. This process, known as “instruction scheduling,” is guaranteed to preserve the logical results of your code, but as can be seen from the code listing below, it will invariably make it harder for you to understand the generated code. Even in the context of a simple function such as main in our example, this can make reading the code challenging. We have annotated this example to show the purpose of each instruction; this output is not provided by the Alpha Visual C++ compiler. Instructions not annotated concern themselves with the housekeeping details required by the Alpha calling standard.

  • The instructions at offset 0 and 4 were inserted by the compiler to load the operands of the zapnot instructions. Similarly, the compiler inserted the load address (lda) instruction at offset 10 to expand the stack frame to contain the automatic variables contained in main.

  • A slightly more sophisticated transformation took place to generate the store long (stl) instruction at offset 2C. Note that while the stl instruction coded in the body of the _ _asm statement referred to a target operand of 0(%2), the compiler was able to directly convert this target operand into a reference to the automatic variable named result, stored at 8(sp).
              0000  main
245FF000  0000  ldah   m1,-4096(zero)        // asm setup
223F1010  0004  mov    4112,a1                // asm setup
4841F630  0008  zapnot m1,15,a0                // asm body
4A21F631  000C  zapnot a1,15,a1                // asm body
23DEFFF0  0010  lda    sp,-16(sp)
4E110401  0014  mulq   a0,a1,t0                // asm body
B75E0000  0018  stq    ra,(sp)
261F0000  001C  ldah   a0,h^.data(zero)    // printf:
22100000  0020  lda    a0,l^.data(a0)        // arg 1
47E20411  0024  mov    m1,a1                    // arg 2
225F1010  0028  mov    4112,a2                // arg 3
B03E0008  002C  stl    t0,8(sp)                // asm body
48241680  0030  srl    t0,32,v0                // asm body
47DF081E  0034  xor    sp,zero,sp            // printf:
A29E0008  0038  ldl    a4,8(sp)                // arg 5
43E00013  003C  sextl  v0,a3                    // arg 4
D3400000  0040  bsr    ra,j^printf            // printf()
A75E0000  0044  ldq    ra,(sp)
47FF0400  0048  clr    v0
23DE0010  004C  lda    sp,16(sp)
6BFA8001  0050  ret    ra


Example 2: Alpha Macro Using Inline Assembly

The second example shows a macro that implements a simple maximum function and a sample driver program.

// int maximum (int a, int b) //
// Action: return max(a,b)    //

#define maximum(a,b)
   __asm("bis %0,%0,v0;"      // assume result:a
      "cmplt v0,%1,t0;"       // a < b ?
      "cmovne t0,%1,v0",a,b)  // yes - result:b

void main()
{
   int i = 1,j = 2;

   printf("max(i,j) = %d\n", maximum(i,j));
}

The code generated for main in this example looks like the following:

0000  main::
47E03410  0000  mov    1,a0               // load i
46003400  0004  bis    a0,1,v0            // asm body
400059A1  0008  cmplt  v0,2,t0            // asm body
23DEFFF0  000C  lda    sp,-16(sp)
442054C0  0010  cmovne t0,2,v0            // asm body
261F0000  0014  ldah   a0,h^.data(zero)   // printf: “”
B75E0000  0018  stq    ra,(sp)
47E00411  001C  mov    v0,a1              // printf: %d
22100000  0020  lda    a0,l^.data(a0)     // printf: “”
D3400000  0024  bsr    ra,j^printf        // printf()
A75E0000  0028  ldq    ra,(sp)
47FF0400  002C  clr    v0
23DE0010  0030  lda    sp,16(sp)
6BFA8001  0034  ret    ra

This example illustrates several additional points about _ _asm statements:

  • Two arguments are passed to the _ _asm statement: a and b. These arguments are referred to in the assembly-language instructions as %0 and %1. The same two arguments also happen to be the arguments to the maximum macro itself, although such agreement in argument lists is not required.

  • In another optimization, the compiler was able to propagate the value for the variable named j (2) directly into the compare less-than (cmplt) and conditional move not-equal (cmovne) instructions specified in the body of the _ _asm statement.